From 74622ced6e634a5c444190400b84f41a605f4db7 Mon Sep 17 00:00:00 2001 From: Xaneets Date: Sun, 17 Aug 2025 21:23:36 +0300 Subject: [PATCH 1/5] todo --- src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 5594fe6..c3d92fa 100644 --- a/src/client.rs +++ b/src/client.rs @@ -117,7 +117,7 @@ impl Client { res.json_verbose().await.map_err(Into::into) } - pub async fn send_backup_by_bot(&self) -> Result<()> { + pub async fn send_backup_by_bot(&self) -> Result<()> { // todo tests let path = vec!["createbackup"]; let res = self.client.get(self.gen_url(path)?).send().await?; if res.status() != StatusCode::OK { @@ -126,7 +126,7 @@ impl Client { Ok(()) } - pub async fn get_client_ips(&self, client_email: String) -> Result { + pub async fn get_client_ips(&self, client_email: String) -> Result { // todo tests let path = vec!["clientIps", &client_email]; let res = self.client.post(self.gen_url(path)?).send().await?; res.json_verbose().await.map_err(Into::into) From 8887abc7d2f86b44ce0b86147bef51258c9fd78b Mon Sep 17 00:00:00 2001 From: Xaneets Date: Mon, 22 Sep 2025 22:05:27 +0300 Subject: [PATCH 2/5] new api methods --- .github/workflows/e2e.yml | 4 +- Cargo.toml | 2 +- README.md | 30 +++++- src/client.rs | 188 +++++++++++++++++++++++++++++++++---- src/lib.rs | 9 +- src/models.rs | 26 +++++- tests/e2e.rs | 191 +++++++++++++++++++++++++++++++++++++- tests/e2e_local.sh | 2 +- 8 files changed, 420 insertions(+), 32 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7f77fd7..363f5bd 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -33,9 +33,9 @@ jobs: - name: Start 3x-ui (Docker) run: | set -euxo pipefail - docker pull ghcr.io/mhsanaei/3x-ui:v2.6.6 + docker pull ghcr.io/mhsanaei/3x-ui:v2.8.3 docker rm -f xui >/dev/null 2>&1 || true - docker run -d --name xui --network host ghcr.io/mhsanaei/3x-ui:v2.6.6 + docker run -d --name xui --network host ghcr.io/mhsanaei/3x-ui:v2.8.3 for i in $(seq 1 90); do code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ diff --git a/Cargo.toml b/Cargo.toml index 2e344d9..f382b94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ categories = ["development-tools"] [dependencies] futures = "0.3.31" log = "0.4.25" -reqwest = { version = "0.12.12", features = ["json", "cookies"] } +reqwest = { version = "0.12.12", features = ["json", "cookies", "multipart"] } serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.138" serde_path_to_error = "0.1.17" diff --git a/README.md b/README.md index 51c2ee5..980fe4c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ Unofficial Rust client for the **3x-ui** panel API (Xray-core). Provides typed models and high-level methods for common panel operations. -> Note: Some 3x-ui endpoints expect certain nested structures to be sent as **JSON strings** (e.g., inbound `settings`). The client models handle these specifics transparently. +> Note: Some 3x-ui endpoints expect certain nested structures to be sent as **JSON strings** (e.g., inbound `settings`). +> The client models handle these specifics transparently. --- @@ -32,7 +33,32 @@ Provides typed models and high-level methods for common panel operations. - [x] Delete inbound - [x] Delete depleted clients - [x] Online clients - +- [ ] Import inbounds +- [x] Last online +- [ ] del Client By Email +- [x] Server status +- [x] Server get DB +- [x] get Xray Version +- [x] get Config Json +- [x] cpu History +- [x] get New UUID +- [x] get New X25519 Cert +- [x] get New mldsa65 +- [x] get New mlkem768 +- [x] get New Vless Enc +- [x] stop Xray Service +- [x] restart Xray Service +- [x] install Xray version +- [x] update Geofile +- [ ] updateGeofile/{fileName} +- [ ] logs/{count} +- [ ] xraylogs/{count} +- [x] importDB +- [ ] get New Ech Cert + + +- ✅ +- ❌ --- ## Installation diff --git a/src/client.rs b/src/client.rs index c3d92fa..5995ae4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,11 +1,16 @@ +#![allow(dead_code)] + use super::{ - ClientIpsResponse, ClientsStatsResponse, ClientsStatsVecResponse, DeleteInboundResponse, - InboundResponse, InboundsResponse, NullObjectResponse, OnlineClientsResponse, Result, + ClientIpsResponse, ClientsStatsResponse, ClientsStatsVecResponse, CpuHistoryResponse, + DeleteInboundResponse, InboundResponse, InboundsResponse, JsonResponse, NullObjectResponse, + OnlineClientsResponse, OptStringVecResponse, Result, StringResponse, StringVecResponse, + UuidRespose, }; use crate::error::Error; -use crate::models::{ClientRequest, CreateInboundRequest}; +use crate::models::{ClientRequest, CreateInboundRequest, Inbounds}; use crate::response_ext::ResponseJsonVerboseExt; -use log::{debug, error}; +use log::debug; +use reqwest::multipart::{Form, Part}; use reqwest::{Client as RClient, IntoUrl, StatusCode, Url}; use serde::Serialize; @@ -39,23 +44,30 @@ impl Client { Ok(client) } - fn gen_url(&self, segs: Vec<&str>) -> Result { - // todo paths to hashmap or enum - let mut base_segs = vec!["panel", "api", "inbounds"]; - base_segs.extend(segs); - let base = self.url.as_str().trim_end_matches('/'); - let mut url = Url::parse(base).map_err(|_| Error::InvalidUrl("Invalid base URL".into()))?; - + fn gen_url_with_base(&self, base: &[&str], segs: Vec<&str>) -> Result { + let base_str = self.url.as_str().trim_end_matches('/'); + let mut url = + Url::parse(base_str).map_err(|_| Error::InvalidUrl("Invalid base URL".into()))?; { let mut path_segments = url .path_segments_mut() .map_err(|_| Error::InvalidUrl("Cannot be a base URL".into()))?; - path_segments.extend(base_segs); + path_segments.extend(base.iter().copied()); + path_segments.extend(segs); } debug!("Generated URL: {}", url); Ok(url) } + fn gen_server_url(&self, segs: Vec<&str>) -> Result { + self.gen_url_with_base(&["panel", "api", "server"], segs) + } + + fn gen_url(&self, segs: Vec<&str>) -> Result { + let base_segs = vec!["panel", "api", "inbounds"]; + self.gen_url_with_base(&base_segs, segs) + } + async fn login(&self) -> Result { #[derive(Serialize)] struct LoginRequest { @@ -89,11 +101,7 @@ impl Client { pub async fn get_inbounds_list(&self) -> Result { let path = vec!["list"]; let res = self.client.get(self.gen_url(path)?).send().await?; - let res = res.json().await.map_err(|e| { - error!("{e}"); - e - })?; - Ok(res) + res.json_verbose().await.map_err(Into::into) } pub async fn get_inbound_by_id(&self, inbound_id: u64) -> Result { @@ -117,7 +125,8 @@ impl Client { res.json_verbose().await.map_err(Into::into) } - pub async fn send_backup_by_bot(&self) -> Result<()> { // todo tests + pub async fn send_backup_by_bot(&self) -> Result<()> { + // todo tests let path = vec!["createbackup"]; let res = self.client.get(self.gen_url(path)?).send().await?; if res.status() != StatusCode::OK { @@ -126,7 +135,8 @@ impl Client { Ok(()) } - pub async fn get_client_ips(&self, client_email: String) -> Result { // todo tests + pub async fn get_client_ips(&self, client_email: String) -> Result { + // todo tests let path = vec!["clientIps", &client_email]; let res = self.client.post(self.gen_url(path)?).send().await?; res.json_verbose().await.map_err(Into::into) @@ -215,4 +225,144 @@ impl Client { let res = self.client.post(url).send().await?; res.json_verbose().await.map_err(Into::into) } + + async fn import_inbound(&self, inbound: &Inbounds) -> Result { + let url = self.gen_url(vec!["import"])?; + let json_str = serde_json::to_string(inbound) + .map_err(|e| Error::OtherError(format!("serialize inbound: {e}")))?; + let form = Form::new().text("data", json_str); + let res = self.client.post(url).multipart(form).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn get_last_online(&self) -> Result { + let url = self.gen_url(vec!["onlines"])?; + let res = self.client.post(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + async fn del_client_by_email( + &self, + inbound_id: u64, + email: &str, + ) -> Result { + let url = self.gen_url(vec![&inbound_id.to_string(), "delClientByEmail", email])?; + let res = self.client.post(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn server_status(&self) -> Result { + let url = self.gen_server_url(vec!["status"])?; + let res = self.client.get(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn server_get_db(&self) -> Result> { + let url = self.gen_server_url(vec!["getDb"])?; + let res = self.client.get(url).send().await?; + Ok(res.bytes().await?.to_vec()) + } + + pub async fn get_xray_version(&self) -> Result { + let url = self.gen_server_url(vec!["getXrayVersion"])?; + let res = self.client.get(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn get_config_json(&self) -> Result { + let url = self.gen_server_url(vec!["getConfigJson"])?; + let res = self.client.get(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn cpu_history(&self, minutes: u32) -> Result { + let url = self.gen_server_url(vec!["cpuHistory", &minutes.to_string()])?; + let res = self.client.get(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn get_new_uuid(&self) -> Result { + let url = self.gen_server_url(vec!["getNewUUID"])?; + let res = self.client.get(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn get_new_x25519_cert(&self) -> Result { + let url = self.gen_server_url(vec!["getNewX25519Cert"])?; + let res = self.client.get(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn get_new_mldsa65(&self) -> Result { + let url = self.gen_server_url(vec!["getNewmldsa65"])?; + let res = self.client.get(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn get_new_mlkem768(&self) -> Result { + let url = self.gen_server_url(vec!["getNewmlkem768"])?; + let res = self.client.get(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn get_new_vless_enc(&self) -> Result { + let url = self.gen_server_url(vec!["getNewVlessEnc"])?; + let res = self.client.get(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + async fn get_new_ech_cert(&self) -> Result { + let url = self.gen_server_url(vec!["getNewEchCert"])?; + let res = self.client.get(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn stop_xray_service(&self) -> Result { + let url = self.gen_server_url(vec!["stopXrayService"])?; + let res = self.client.post(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn restart_xray_service(&self) -> Result { + let url = self.gen_server_url(vec!["restartXrayService"])?; + let res = self.client.post(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn install_xray_version(&self, version: &str) -> Result { + let url = self.gen_server_url(vec!["installXray", version])?; + let res = self.client.post(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn update_geofile(&self) -> Result { + let url = self.gen_server_url(vec!["updateGeofile"])?; + let res = self.client.post(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + async fn update_geofile_by_name(&self, file_name: &str) -> Result { + let url = self.gen_server_url(vec!["updateGeofile", file_name])?; + let res = self.client.post(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + async fn logs(&self, count: u32) -> Result { + let url = self.gen_server_url(vec!["logs", &count.to_string()])?; + let res = self.client.get(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + async fn xray_logs(&self, count: u32) -> Result { + let url = self.gen_server_url(vec!["xraylogs", &count.to_string()])?; + let res = self.client.get(url).send().await?; + res.json_verbose().await.map_err(Into::into) + } + + pub async fn import_db_upload(&self, filename: &str, bytes: Vec) -> Result { + let url = self.gen_server_url(vec!["importDB"])?; + let form = Form::new().part("db", Part::bytes(bytes).file_name(filename.to_string())); + let res = self.client.post(url).multipart(form).send().await?; + res.json_verbose().await.map_err(Into::into) + } } diff --git a/src/lib.rs b/src/lib.rs index a8ea1b7..fb9e177 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,10 @@ extern crate core; use crate::error::Error; -use crate::models::Response; +use crate::models::{CpuHistoryPoint, Response, Uuid}; pub use client::Client; use models::{ClientStats, Inbounds}; +use serde_json::Value; pub mod client; pub mod error; @@ -21,3 +22,9 @@ pub type ClientsStatsResponse = Response; pub type ClientIpsResponse = Response; // todo ip struct | result [ip, ip] or No ip record string custom deserializer pub type DeleteInboundResponse = Response; pub type OnlineClientsResponse = Response>>; +pub type StringResponse = Response; +pub type JsonResponse = Response; +pub type OptStringVecResponse = Response>>; +pub type StringVecResponse = Response>; +pub type CpuHistoryResponse = Response>; +pub type UuidRespose = Response; diff --git a/src/models.rs b/src/models.rs index 7cb3183..00d3247 100644 --- a/src/models.rs +++ b/src/models.rs @@ -22,7 +22,7 @@ impl Response { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct ClientStats { pub id: u64, #[serde(rename = "inboundId")] @@ -37,7 +37,7 @@ pub struct ClientStats { pub reset: i64, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Inbounds { pub id: u64, pub up: u128, @@ -58,7 +58,7 @@ pub struct Inbounds { pub stream_settings: String, // todo pub tag: String, pub sniffing: String, // todo - pub allocate: String, // todo + pub allocate: Option, // todo } #[serde_as] @@ -108,11 +108,18 @@ pub struct User { pub total_gb: u32, pub expiry_time: u64, pub enable: bool, - pub tg_id: String, + pub tg_id: TgId, pub sub_id: String, pub reset: u32, } +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum TgId { + String(String), + Int(u32), +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Fallback { @@ -137,3 +144,14 @@ pub struct ClientRequest { #[serde_as(as = "JsonString<_>")] pub settings: ClientSettings, } + +#[derive(Debug, Serialize, Deserialize)] +pub struct CpuHistoryPoint { + pub cpu: f64, + pub t: u64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Uuid { + pub uuid: String +} \ No newline at end of file diff --git a/tests/e2e.rs b/tests/e2e.rs index ee8bf65..2c232c1 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -3,6 +3,7 @@ use std::env; use tokio::time::{Duration, sleep}; use uuid::Uuid; +use rustix3::models::TgId; use rustix3::{ client::Client, inbounds::InboundProtocols, @@ -82,7 +83,7 @@ async fn e2e_full_flow() { total_gb: 0, expiry_time: 0, enable: true, - tg_id: String::new(), + tg_id: TgId::Int(0), sub_id: String::new(), reset: 0, }; @@ -164,7 +165,7 @@ async fn e2e_full_flow() { total_gb: 0, expiry_time: 0, enable: true, - tg_id: String::new(), + tg_id: TgId::Int(0), sub_id: String::new(), reset: 0, }; @@ -207,4 +208,190 @@ async fn e2e_full_flow() { let list_after = client.get_inbounds_list().await.expect("list_after"); assert!(list_after.is_ok()); log::info!("list_after = {:#?}", list_after); + + let last_online = client.get_last_online().await.expect("last_online"); + assert!(last_online.is_ok()); + + let cuuid = Uuid::new_v4().to_string(); + let email = "testclient".to_string(); + let user_obj = User { + id: cuuid.clone(), + flow: String::new(), + email: email.clone(), + limit_ip: 0, + total_gb: 0, + expiry_time: 0, + enable: true, + tg_id: TgId::Int(0), + sub_id: String::new(), + reset: 0, + }; + + let remark2 = format!("e2e-del-by-email-{}", Uuid::new_v4()); + let tmp_inb_req = CreateInboundRequest { + up: 0, + down: 0, + total: 0, + remark: remark2.clone(), + enable: true, + expiry_time: 0, + listen: String::new(), + port: 31002, + protocol: InboundProtocols::Vless, + settings: Settings { + clients: vec![user_obj.clone()], + decryption: "none".into(), + fallbacks: Vec::::new(), + }, + stream_settings: "{}".into(), + sniffing: "{}".into(), + allocate: "{}".into(), + }; + let tmp_created = client + .add_inbound(&tmp_inb_req) + .await + .expect("add_inbound_tmp"); + assert!(tmp_created.is_ok()); + let tmp_inbound_id = tmp_created.object.id; + + let tmp = client.get_inbounds_list().await.expect("tmp inbound"); + assert!(tmp.is_ok()); + log::info!("tmp inbound = {:#?}", tmp); + + // let del_by_email = client + // .del_client_by_email(tmp_inbound_id, &email) + // .await + // .expect("del_client_by_email"); + // assert!(del_by_email.is_ok()); // todo + + let res = client + .delete_inbound(tmp_inbound_id) + .await + .expect("del_tmp_inbound"); + + assert!(res.is_ok()); + + let srv_status = client.server_status().await.expect("server_status"); + assert!(srv_status.is_ok()); + + let db_bytes = client.server_get_db().await.expect("server_get_db"); + assert!(!db_bytes.is_empty(), "db should not be empty"); + + + let imported_db = client + .import_db_upload("file", db_bytes.clone()) + .await + .expect("import_db_upload"); + assert!(imported_db.is_ok()); + + let xver = client.get_xray_version().await.expect("xray_version"); + assert!(xver.is_ok()); + let current_version = xver.object.clone(); + + let cfg = client.get_config_json().await.expect("get_config_json"); + assert!(cfg.is_ok()); + + let cpu_hist = client.cpu_history(2).await.expect("cpu_history_1min"); // todo bucket + assert!(cpu_hist.is_ok()); + + + if let Some(first) = cpu_hist.object.first() { + assert!(first.t > 0, "cpu history timestamp should be > 0"); + } + + let new_uuid = client.get_new_uuid().await.expect("get_new_uuid"); + assert!(new_uuid.is_ok()); + + let parsed = Uuid::parse_str(&new_uuid.object.uuid); + assert!(parsed.is_ok(), "server UUID should be valid"); + + let x25519 = client.get_new_x25519_cert().await.expect("get_new_x25519"); + assert!(x25519.is_ok()); + + let mldsa = client.get_new_mldsa65().await.expect("get_new_mldsa65"); + assert!(mldsa.is_ok()); + + let mlkem = client.get_new_mlkem768().await.expect("get_new_mlkem768"); + assert!(mlkem.is_ok()); + + let venc = client.get_new_vless_enc().await.expect("get_new_vless_enc"); + assert!(venc.is_ok()); + + // let ech = client.get_new_ech_cert().await.expect("get_new_ech_cert"); + // assert!(ech.is_ok()); // todo + + let stopped = client.stop_xray_service().await.expect("stop_xray_service"); + assert!(stopped.is_ok()); + + sleep(Duration::from_secs(1)).await; + + let restarted = client + .restart_xray_service() + .await + .expect("restart_xray_service"); + assert!(restarted.is_ok()); + + sleep(Duration::from_secs(2)).await; + + log::info!("ver: {:#?}", current_version.get(0).expect("version")); + + let reinstall = client + .install_xray_version(current_version.get(0).expect("version")) + .await + .expect("install_xray_version"); + assert!(reinstall.is_ok()); + + let geo_all = client.update_geofile().await.expect("update_geofile"); + assert!(geo_all.is_ok()); + + // let geo_one = client + // .update_geofile_by_name("geoip") + // .await + // .expect("update_geofile_by_name"); + // assert!(geo_one.is_ok()); // todo + + // let logs = client.logs(50).await.expect("logs_count"); + // assert!(logs.is_ok()); // todo + // может прийти пустая строка — просто проверим, что ответ формально ок + + // let xlogs = client.xray_logs(50).await.expect("xray_logs_count"); + // assert!(xlogs.is_ok()); // todo + + + let remark = format!("e2e-{}", Uuid::new_v4()); + let req = CreateInboundRequest { + up: 0, + down: 0, + total: 0, + remark: remark.clone(), + enable: true, + expiry_time: 0, + listen: String::new(), + port: 31001, + protocol: InboundProtocols::Vless, + settings: Settings { + clients: vec![], + decryption: "none".into(), + fallbacks: Vec::::new(), + }, + stream_settings: "{}".into(), + sniffing: "{}".into(), + allocate: "{}".into(), + }; + + let created = client.add_inbound(&req).await.expect("add_inbound"); + + assert!(created.is_ok()); + + + let inbds = client.get_inbounds_list().await.expect("list_for_import"); + assert!(inbds.is_ok()); + + log::info!("{:#?}", inbds); + + // let import_inb = client // todo fix cannot unmarshal object into Go struct field Inbound.settings of type string + // .import_inbound(&inbds.object.get(0).expect("object")) + // .await + // .expect("import_inbounds"); + // assert!(import_inb.is_ok()); } diff --git a/tests/e2e_local.sh b/tests/e2e_local.sh index 458e0cd..736a594 100755 --- a/tests/e2e_local.sh +++ b/tests/e2e_local.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -IMG="ghcr.io/mhsanaei/3x-ui:v2.6.6" +IMG="ghcr.io/mhsanaei/3x-ui:v2.8.3" NAME="xui" BASE="http://127.0.0.1:2053" USER="admin" From 6ee5246fc2239cc5ee9d3400708df262444a2350 Mon Sep 17 00:00:00 2001 From: Xaneets Date: Mon, 22 Sep 2025 22:06:39 +0300 Subject: [PATCH 3/5] upd readme --- README.md | 88 +++++++++++++++++++++++++++---------------------------- 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 980fe4c..52474f2 100644 --- a/README.md +++ b/README.md @@ -14,51 +14,49 @@ Provides typed models and high-level methods for common panel operations. ## Implemented endpoints -- [x] login -- [x] Inbounds -- [x] Inbound -- [x] Client traffics with email -- [x] Client traffics with id -- [x] TG Send backup to admin -- [x] Client IP address -- [x] Add inbound -- [x] Add client to inbound -- [x] Update inbound -- [x] Update client -- [x] Clear client IP address -- [x] Reset traffics of all inbound -- [x] Reset traffics of all clients in an inbound -- [x] Reset client traffics -- [x] Delete client -- [x] Delete inbound -- [x] Delete depleted clients -- [x] Online clients -- [ ] Import inbounds -- [x] Last online -- [ ] del Client By Email -- [x] Server status -- [x] Server get DB -- [x] get Xray Version -- [x] get Config Json -- [x] cpu History -- [x] get New UUID -- [x] get New X25519 Cert -- [x] get New mldsa65 -- [x] get New mlkem768 -- [x] get New Vless Enc -- [x] stop Xray Service -- [x] restart Xray Service -- [x] install Xray version -- [x] update Geofile -- [ ] updateGeofile/{fileName} -- [ ] logs/{count} -- [ ] xraylogs/{count} -- [x] importDB -- [ ] get New Ech Cert - - -- ✅ -- ❌ +- ✅ login +- ✅ Inbounds +- ✅ Inbound +- ✅ Client traffics with email +- ✅ Client traffics with id +- ✅ TG Send backup to admin +- ✅ Client IP address +- ✅ Add inbound +- ✅ Add client to inbound +- ✅ Update inbound +- ✅ Update client +- ✅ Clear client IP address +- ✅ Reset traffics of all inbound +- ✅ Reset traffics of all clients in an inbound +- ✅ Reset client traffics +- ✅ Delete client +- ✅ Delete inbound +- ✅ Delete depleted clients +- ✅ Online clients +- ❌ Import inbounds +- ✅ Last online +- ❌ del Client By Email +- ✅ Server status +- ✅ Server get DB +- ✅ get Xray Version +- ✅ get Config Json +- ✅ cpu History +- ✅ get New UUID +- ✅ get New X25519 Cert +- ✅ get New mldsa65 +- ✅ get New mlkem768 +- ✅ get New Vless Enc +- ✅ stop Xray Service +- ✅ restart Xray Service +- ✅ install Xray version +- ✅ update Geofile +- ❌ updateGeofile/{fileName} +- ❌ logs/{count} +- ❌ xraylogs/{count} +- ✅ importDB +- ❌ get New Ech Cert + + --- ## Installation From c0a9e7d8b27c23ecfd5b7f191c38688dee4cc317 Mon Sep 17 00:00:00 2001 From: Xaneets Date: Mon, 22 Sep 2025 22:11:31 +0300 Subject: [PATCH 4/5] version 0.4.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f382b94..a8aa06a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustix3" -version = "0.3.0" +version = "0.4.0" edition = "2024" authors = ["Dmitriy Sergeev "] description = "API lib for 3x-ui panel" From a7eee3c1eff47a9df68763e46d31b497a30399ac Mon Sep 17 00:00:00 2001 From: Xaneets Date: Mon, 22 Sep 2025 22:31:19 +0300 Subject: [PATCH 5/5] fix method name --- Cargo.toml | 1 + src/client.rs | 44 ++++++++++++++++++++++---------------------- tests/e2e.rs | 1 - 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a8aa06a..77bcf4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ homepage = "https://github.com/Xaneets/rustix3" keywords = ["x-ui", "3x-ui", "api", "xray-core", "vpn"] categories = ["development-tools"] + [dependencies] futures = "0.3.31" log = "0.4.25" diff --git a/src/client.rs b/src/client.rs index 5995ae4..379b24d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -63,7 +63,7 @@ impl Client { self.gen_url_with_base(&["panel", "api", "server"], segs) } - fn gen_url(&self, segs: Vec<&str>) -> Result { + fn gen_inbounds_url(&self, segs: Vec<&str>) -> Result { let base_segs = vec!["panel", "api", "inbounds"]; self.gen_url_with_base(&base_segs, segs) } @@ -100,20 +100,20 @@ impl Client { pub async fn get_inbounds_list(&self) -> Result { let path = vec!["list"]; - let res = self.client.get(self.gen_url(path)?).send().await?; + let res = self.client.get(self.gen_inbounds_url(path)?).send().await?; res.json_verbose().await.map_err(Into::into) } pub async fn get_inbound_by_id(&self, inbound_id: u64) -> Result { let id = inbound_id.to_string(); let path = vec!["get", &id]; - let res = self.client.get(self.gen_url(path)?).send().await?; + let res = self.client.get(self.gen_inbounds_url(path)?).send().await?; res.json_verbose().await.map_err(Into::into) } pub async fn get_client_traffic_by_email(&self, email: String) -> Result { let path = vec!["getClientTraffics", &email]; - let res = self.client.get(self.gen_url(path)?).send().await?; // todo check is null return user not found + let res = self.client.get(self.gen_inbounds_url(path)?).send().await?; // todo check is null return user not found res.json_verbose().await.map_err(Into::into) } @@ -121,14 +121,14 @@ impl Client { // todo id to uuid let id = id.to_string(); let path = vec!["getClientTrafficsById", &id]; - let res = self.client.get(self.gen_url(path)?).send().await?; + let res = self.client.get(self.gen_inbounds_url(path)?).send().await?; res.json_verbose().await.map_err(Into::into) } pub async fn send_backup_by_bot(&self) -> Result<()> { // todo tests let path = vec!["createbackup"]; - let res = self.client.get(self.gen_url(path)?).send().await?; + let res = self.client.get(self.gen_inbounds_url(path)?).send().await?; if res.status() != StatusCode::OK { return Err(Error::OtherError("Todo".into())); } @@ -138,18 +138,18 @@ impl Client { pub async fn get_client_ips(&self, client_email: String) -> Result { // todo tests let path = vec!["clientIps", &client_email]; - let res = self.client.post(self.gen_url(path)?).send().await?; + let res = self.client.post(self.gen_inbounds_url(path)?).send().await?; res.json_verbose().await.map_err(Into::into) } pub async fn add_inbound(&self, req: &CreateInboundRequest) -> Result { - let url = self.gen_url(vec!["add"])?; + let url = self.gen_inbounds_url(vec!["add"])?; let res = self.client.post(url).json(req).send().await?; res.json_verbose().await.map_err(Into::into) } pub async fn add_client_to_inbound(&self, req: &ClientRequest) -> Result { - let url = self.gen_url(vec!["addClient"])?; + let url = self.gen_inbounds_url(vec!["addClient"])?; let res = self.client.post(url).json(req).send().await?; res.json_verbose().await.map_err(Into::into) } @@ -159,7 +159,7 @@ impl Client { inbound_id: u64, req: &CreateInboundRequest, ) -> Result { - let url = self.gen_url(vec!["update", &inbound_id.to_string()])?; + let url = self.gen_inbounds_url(vec!["update", &inbound_id.to_string()])?; let res = self.client.post(url).json(req).send().await?; res.json_verbose().await.map_err(Into::into) } @@ -169,25 +169,25 @@ impl Client { uuid: &str, req: &ClientRequest, ) -> Result { - let url = self.gen_url(vec!["updateClient", uuid])?; + let url = self.gen_inbounds_url(vec!["updateClient", uuid])?; let res = self.client.post(url).json(req).send().await?; res.json_verbose().await.map_err(Into::into) } pub async fn clear_client_ips(&self, email: &str) -> Result { - let url = self.gen_url(vec!["clearClientIps", email])?; + let url = self.gen_inbounds_url(vec!["clearClientIps", email])?; let res = self.client.post(url).send().await?; res.json_verbose().await.map_err(Into::into) } pub async fn reset_all_inbound_traffics(&self) -> Result { - let url = self.gen_url(vec!["resetAllTraffics"])?; + let url = self.gen_inbounds_url(vec!["resetAllTraffics"])?; let res = self.client.post(url).send().await?; res.json_verbose().await.map_err(Into::into) } pub async fn reset_all_client_traffics(&self, inbound_id: u64) -> Result { - let url = self.gen_url(vec!["resetAllClientTraffics", &inbound_id.to_string()])?; + let url = self.gen_inbounds_url(vec!["resetAllClientTraffics", &inbound_id.to_string()])?; let res = self.client.post(url).send().await?; res.json_verbose().await.map_err(Into::into) } @@ -197,37 +197,37 @@ impl Client { inbound_id: u64, email: &str, ) -> Result { - let url = self.gen_url(vec![&inbound_id.to_string(), "resetClientTraffic", email])?; + let url = self.gen_inbounds_url(vec![&inbound_id.to_string(), "resetClientTraffic", email])?; let res = self.client.post(url).send().await?; res.json_verbose().await.map_err(Into::into) } pub async fn delete_client(&self, inbound_id: u64, uuid: &str) -> Result { - let url = self.gen_url(vec![&inbound_id.to_string(), "delClient", uuid])?; + let url = self.gen_inbounds_url(vec![&inbound_id.to_string(), "delClient", uuid])?; let res = self.client.post(url).send().await?; res.json_verbose().await.map_err(Into::into) } pub async fn delete_inbound(&self, inbound_id: u64) -> Result { - let url = self.gen_url(vec!["del", &inbound_id.to_string()])?; + let url = self.gen_inbounds_url(vec!["del", &inbound_id.to_string()])?; let res = self.client.post(url).send().await?; res.json_verbose().await.map_err(Into::into) } pub async fn delete_depleted_clients(&self, inbound_id: u64) -> Result { - let url = self.gen_url(vec!["delDepletedClients", &inbound_id.to_string()])?; + let url = self.gen_inbounds_url(vec!["delDepletedClients", &inbound_id.to_string()])?; let res = self.client.post(url).send().await?; res.json_verbose().await.map_err(Into::into) } pub async fn online_clients(&self) -> Result { - let url = self.gen_url(vec!["onlines"])?; + let url = self.gen_inbounds_url(vec!["onlines"])?; let res = self.client.post(url).send().await?; res.json_verbose().await.map_err(Into::into) } async fn import_inbound(&self, inbound: &Inbounds) -> Result { - let url = self.gen_url(vec!["import"])?; + let url = self.gen_inbounds_url(vec!["import"])?; let json_str = serde_json::to_string(inbound) .map_err(|e| Error::OtherError(format!("serialize inbound: {e}")))?; let form = Form::new().text("data", json_str); @@ -236,7 +236,7 @@ impl Client { } pub async fn get_last_online(&self) -> Result { - let url = self.gen_url(vec!["onlines"])?; + let url = self.gen_inbounds_url(vec!["onlines"])?; let res = self.client.post(url).send().await?; res.json_verbose().await.map_err(Into::into) } @@ -246,7 +246,7 @@ impl Client { inbound_id: u64, email: &str, ) -> Result { - let url = self.gen_url(vec![&inbound_id.to_string(), "delClientByEmail", email])?; + let url = self.gen_inbounds_url(vec![&inbound_id.to_string(), "delClientByEmail", email])?; let res = self.client.post(url).send().await?; res.json_verbose().await.map_err(Into::into) } diff --git a/tests/e2e.rs b/tests/e2e.rs index 2c232c1..ede79cb 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -352,7 +352,6 @@ async fn e2e_full_flow() { // let logs = client.logs(50).await.expect("logs_count"); // assert!(logs.is_ok()); // todo - // может прийти пустая строка — просто проверим, что ответ формально ок // let xlogs = client.xray_logs(50).await.expect("xray_logs_count"); // assert!(xlogs.is_ok()); // todo