Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ Cargo.lock

**/*.rs.bk

.idea/
.idea/

3X-UI.postman_collection.json
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rustix3"
version = "0.5.0"
version = "0.6.0"
edition = "2024"
authors = ["Dmitriy Sergeev <xaneets@gmail.com>"]
description = "API lib for 3x-ui panel"
Expand All @@ -21,6 +21,7 @@ serde_json = "1.0.138"
serde_path_to_error = "0.1.17"
serde_with = { version = "3.14.0", features = ["json"] }
thiserror = "2.0.11"
tokio = { version = "1", features = ["time"] }
uuid = { version = "1", features = ["v4", "serde"] }


Expand Down
166 changes: 165 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,168 @@ async fn main() -> anyhow::Result<()> {
Ok(())
}

```
```

## Configure retry and timeouts

```rust
use rustix3::{Client, ClientOptions};
use reqwest::Method;
use std::time::Duration;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut options = ClientOptions::default();
options.retry_count = 3;
options.retry_base_delay = Duration::from_millis(300);
options.retry_max_delay = Duration::from_secs(3);
options.retry_methods = vec![Method::GET, Method::HEAD];
options.connect_timeout = Duration::from_secs(5);
options.request_timeout = Duration::from_secs(20);

let client = Client::new_with_options("admin", "admin", "http://127.0.0.1:2053/", options).await?;
let _ = client.get_inbounds_list().await?;
Ok(())
}
```

## Error handling

All API responses use a `success/msg/obj` envelope. When `success=false`, the client returns
`Error::ApiError { message }` with the server-provided `msg`.

Network and protocol errors are mapped to:
- `Error::InvalidUrl` for malformed base URL
- `Error::NotFound` for HTTP 404
- `Error::Connection` for other reqwest failures
- `Error::JsonVerbose` for JSON decoding errors (includes JSON path)

Example:

```rust
use rustix3::Error;

match client.get_inbounds_list().await {
Ok(inbounds) => println!("count={}", inbounds.len()),
Err(Error::ApiError { message }) => eprintln!("api error: {}", message),
Err(e) => eprintln!("request error: {}", e),
}
```

---

## Create inbound example

```rust
use rustix3::client::Client;
use rustix3::inbounds::InboundProtocols;
use rustix3::inbounds::TransportProtocol;
use rustix3::models::{CreateInboundRequest, SettingsRequest, Fallback, Sniffing, StreamSettings, TcpHeader, TcpSettings};
use serde_json::json;

fn default_stream_settings() -> StreamSettings {
StreamSettings {
network: Some(TransportProtocol::Tcp),
security: Some("none".into()),
external_proxy: Some(Vec::new()),
tcp_settings: Some(TcpSettings {
accept_proxy_protocol: Some(false),
header: Some(TcpHeader {
header_type: Some("none".into()),
extra: Default::default(),
}),
extra: Default::default(),
}),
ws_settings: None,
grpc_settings: None,
kcp_settings: None,
http_upgrade_settings: None,
xhttp_settings: None,
extra: Default::default(),
}
}

fn default_sniffing() -> Sniffing {
Sniffing {
enabled: false,
dest_override: vec![
rustix3::inbounds::SniffingOption::Http,
rustix3::inbounds::SniffingOption::Tls,
rustix3::inbounds::SniffingOption::Quic,
rustix3::inbounds::SniffingOption::FakeDns,
],
metadata_only: false,
route_only: false,
extra: Default::default(),
}
}

fn default_allocate() -> serde_json::Value {
json!({
"strategy": "always",
"refresh": 5,
"concurrency": 3
})
}

async fn create_inbound(client: &Client) -> anyhow::Result<()> {
let req = CreateInboundRequest {
up: 0,
down: 0,
total: 0,
remark: "example-inbound".into(),
enable: true,
expiry_time: 0,
listen: "0.0.0.0".into(),
port: 31001,
protocol: InboundProtocols::Vless,
settings: SettingsRequest {
clients: vec![],
decryption: Some("none".into()),
encryption: Some("none".into()),
fallbacks: Vec::<Fallback>::new(),
},
stream_settings: default_stream_settings(),
sniffing: default_sniffing(),
allocate: default_allocate(),
};

let _created = client.add_inbound(&req).await?;
Ok(())
}
```

## Add client example

```rust
use rustix3::client::Client;
use rustix3::models::{ClientRequest, ClientSettings, UserRequest, TgId};
use uuid::Uuid;

async fn add_client(client: &Client, inbound_id: u64) -> anyhow::Result<()> {
let user_id = Uuid::new_v4().to_string();
let email = format!("{user_id}@example.com");
let sub_id = Uuid::new_v4().simple().to_string();

let user = UserRequest {
id: user_id,
flow: String::new(),
email,
limit_ip: 2,
total_gb: 100,
expiry_time: 0,
enable: true,
tg_id: Some(TgId::Int(0)),
sub_id,
reset: 0,
};

let req = ClientRequest {
id: inbound_id,
settings: ClientSettings { clients: vec![user] },
};

client.add_client_to_inbound(&req).await?;
Ok(())
}
```
Loading
Loading