#![allow(clippy::missing_errors_doc)]
use std::collections::BTreeMap;
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use reqwest::Method;
use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value;
use super::models::{
ApiTokenCreateRequest, ApiTokenMetadata, ApiTokenScope, BalancerStatus, CreatedApiToken,
EffectiveDefaults, FactoryDefaults, GeoCategoryPage, GeoEntryPage, GeoFile, GeodataTokenIssue,
MoveDirection, OutboundDocuments, OutboundSubscription, OutboundSubscriptionInput,
OutboundTestMode, OutboundTestResult, OutboundTraffic, PanelSettingsUpdate, PanelSettingsView,
PiaAccount, PiaCountry, PiaKey, PiaServers, RouteTestRequest, RouteTestResult,
SensitivePayload, SmtpTestResult, UserCredentialsUpdate, WarpRegistration,
XraySettingsSnapshot,
};
use crate::{
Client, Error, Result, XrayConfig, client::AuthenticationScope, response::ApiResponse,
};
const SETTINGS_ROOT: &str = "panel/api/setting";
const XRAY_ROOT: &str = "panel/api/xray";
#[derive(Clone, Copy, Debug)]
pub struct SettingsApi<'client> {
client: &'client Client,
}
impl<'client> SettingsApi<'client> {
pub(crate) const fn new(client: &'client Client) -> Self {
Self { client }
}
pub async fn all(self) -> Result<PanelSettingsView> {
self.post_object("all", None::<&()>).await
}
pub async fn test_discord(self) -> Result<()> {
self.post_empty("testDiscord", None::<&()>).await
}
pub async fn defaults(self) -> Result<EffectiveDefaults> {
self.post_object("defaultSettings", None::<&()>).await
}
pub async fn factory_defaults(self) -> Result<FactoryDefaults> {
self.post_object("factoryDefaults", None::<&()>).await
}
pub async fn update(self, update: &PanelSettingsUpdate) -> Result<()> {
self.post_empty("update", Some(update)).await
}
pub async fn validate_regex(self, regex: &str) -> Result<()> {
#[derive(Serialize)]
struct Body<'a> {
regex: &'a str,
}
self.post_empty("validateRegex", Some(&Body { regex }))
.await
}
pub async fn update_user(self, update: &UserCredentialsUpdate) -> Result<()> {
self.post_empty("updateUser", Some(update)).await
}
pub async fn restart_panel(self) -> Result<()> {
self.post_empty("restartPanel", None::<&()>).await
}
pub async fn default_xray_config(self) -> Result<XrayConfig> {
self.get_object("getDefaultJsonConfig").await
}
pub async fn api_tokens(self) -> Result<Vec<ApiTokenMetadata>> {
self.get_object("apiTokens").await
}
pub async fn create_api_token(
self,
request: &ApiTokenCreateRequest,
) -> Result<CreatedApiToken> {
self.post_form_object("apiTokens/create", request).await
}
pub async fn delete_api_token(self, id: i64, expected_scope: ApiTokenScope) -> Result<()> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {
expected_scope: &'a str,
}
self.post_empty(
&format!("apiTokens/delete/{id}"),
Some(&Body {
expected_scope: expected_scope.as_str(),
}),
)
.await
}
pub async fn set_api_token_enabled(
self,
id: i64,
expected_scope: ApiTokenScope,
enabled: bool,
) -> Result<()> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Body {
enabled: bool,
expected_scope: &'static str,
}
self.post_empty(
&format!("apiTokens/setEnabled/{id}"),
Some(&Body {
enabled,
expected_scope: expected_scope.as_str(),
}),
)
.await
}
pub async fn test_smtp(self) -> Result<SmtpTestResult> {
let path = format!("{SETTINGS_ROOT}/testSmtp");
let result = self
.client
.execute_response::<SmtpTestResult, ()>(
Method::POST,
&path,
None,
AuthenticationScope::PanelApi,
)
.await?;
if !result.success && result.stage.is_empty() {
return Err(Error::Api {
method: Method::POST,
url: Box::new(self.client.endpoint(&path)?),
message: if result.message.is_empty() {
"SMTP test is unavailable".to_owned()
} else {
result.message
},
});
}
Ok(result)
}
pub async fn test_telegram(self) -> Result<()> {
self.post_empty("testTgBot", None::<&()>).await
}
async fn get_object<T>(self, suffix: &str) -> Result<T>
where
T: DeserializeOwned,
{
let path = format!("{SETTINGS_ROOT}/{suffix}");
let envelope = self
.client
.execute::<T, ()>(Method::GET, &path, None, AuthenticationScope::PanelApi)
.await?;
required_object(self.client, Method::GET, &path, envelope)
}
async fn post_object<T, B>(self, suffix: &str, body: Option<&B>) -> Result<T>
where
T: DeserializeOwned,
B: Serialize + ?Sized,
{
let path = format!("{SETTINGS_ROOT}/{suffix}");
let envelope = self
.client
.execute(Method::POST, &path, body, AuthenticationScope::PanelApi)
.await?;
required_object(self.client, Method::POST, &path, envelope)
}
async fn post_form_object<T, B>(self, suffix: &str, form: &B) -> Result<T>
where
T: DeserializeOwned,
B: Serialize + ?Sized,
{
let path = format!("{SETTINGS_ROOT}/{suffix}");
let envelope = self
.client
.execute_form(Method::POST, &path, form, AuthenticationScope::PanelApi)
.await?;
required_object(self.client, Method::POST, &path, envelope)
}
async fn post_empty<B>(self, suffix: &str, body: Option<&B>) -> Result<()>
where
B: Serialize + ?Sized,
{
let path = format!("{SETTINGS_ROOT}/{suffix}");
self.client
.execute::<Value, B>(Method::POST, &path, body, AuthenticationScope::PanelApi)
.await?;
Ok(())
}
}
#[derive(Clone, Copy, Debug)]
pub struct XraySettingsApi<'client> {
client: &'client Client,
}
impl<'client> XraySettingsApi<'client> {
pub(crate) const fn new(client: &'client Client) -> Self {
Self { client }
}
pub async fn default_config(self) -> Result<XrayConfig> {
self.get_object("getDefaultJsonConfig").await
}
pub async fn outbounds_traffic(self) -> Result<Vec<OutboundTraffic>> {
self.get_object("getOutboundsTraffic").await
}
pub async fn xray_result(self) -> Result<String> {
self.get_object("getXrayResult").await
}
pub async fn settings(self) -> Result<XraySettingsSnapshot> {
let path = format!("{XRAY_ROOT}/");
let envelope = self
.client
.execute::<String, ()>(Method::POST, &path, None, AuthenticationScope::PanelApi)
.await?;
let raw = required_object(self.client, Method::POST, &path, envelope)?;
let url = self.client.endpoint(&path)?;
serde_json::from_str(&raw).map_err(|source| Error::Decode {
method: Method::POST,
url: Box::new(url),
source,
})
}
pub async fn update(self, config: &XrayConfig, outbound_test_url: &str) -> Result<()> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Form<'a> {
xray_setting: String,
outbound_test_url: &'a str,
}
let xray_setting = serde_json::to_string(config).map_err(|source| Error::Encode {
operation: "update Xray settings",
source,
})?;
self.post_form_empty(
"update",
&Form {
xray_setting,
outbound_test_url,
},
)
.await
}
pub async fn warp_data(self) -> Result<SensitivePayload> {
self.integration("warp", "data", None::<&()>).await
}
pub async fn delete_warp(self) -> Result<()> {
self.integration_empty("warp", "del", None::<&()>).await
}
pub async fn warp_config(self) -> Result<SensitivePayload> {
self.integration("warp", "config", None::<&()>).await
}
pub async fn register_warp(self, registration: &WarpRegistration) -> Result<SensitivePayload> {
self.integration("warp", "reg", Some(registration)).await
}
pub async fn change_warp_ip(self) -> Result<SensitivePayload> {
self.integration("warp", "changeIp", None::<&()>).await
}
pub async fn set_warp_license(self, license: &str) -> Result<SensitivePayload> {
#[derive(Serialize)]
struct Form<'a> {
license: &'a str,
}
self.integration("warp", "license", Some(&Form { license }))
.await
}
pub async fn set_warp_update_interval(self, interval: u32) -> Result<()> {
#[derive(Serialize)]
struct Form {
interval: u32,
}
self.integration_empty("warp", "interval", Some(&Form { interval }))
.await
}
pub async fn nord_countries(self) -> Result<SensitivePayload> {
self.integration("nord", "countries", None::<&()>).await
}
pub async fn nord_servers(self, country_id: &str) -> Result<SensitivePayload> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Form<'a> {
country_id: &'a str,
}
self.integration("nord", "servers", Some(&Form { country_id }))
.await
}
pub async fn register_nord(self, token: &str) -> Result<SensitivePayload> {
#[derive(Serialize)]
struct Form<'a> {
token: &'a str,
}
self.integration("nord", "reg", Some(&Form { token })).await
}
pub async fn set_nord_key(self, key: &str) -> Result<SensitivePayload> {
#[derive(Serialize)]
struct Form<'a> {
key: &'a str,
}
self.integration("nord", "setKey", Some(&Form { key }))
.await
}
pub async fn nord_data(self) -> Result<SensitivePayload> {
self.integration("nord", "data", None::<&()>).await
}
pub async fn delete_nord(self) -> Result<()> {
self.integration_empty("nord", "del", None::<&()>).await
}
pub async fn pia_countries(self) -> Result<Vec<PiaCountry>> {
self.integration("pia", "countries", None::<&()>).await
}
pub async fn pia_servers(self, country_code: &str) -> Result<PiaServers> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Form<'a> {
country_code: &'a str,
}
self.integration("pia", "servers", Some(&Form { country_code }))
.await
}
pub async fn register_pia(self, username: &str, password: &str) -> Result<PiaAccount> {
#[derive(Serialize)]
struct Form<'a> {
username: &'a str,
password: &'a str,
}
self.integration("pia", "reg", Some(&Form { username, password }))
.await
}
pub async fn pia_data(self) -> Result<PiaAccount> {
self.integration("pia", "data", None::<&()>).await
}
pub async fn delete_pia(self) -> Result<()> {
self.integration_empty("pia", "del", None::<&()>).await
}
pub async fn add_pia_key(self, hostname: &str) -> Result<PiaKey> {
#[derive(Serialize)]
struct Form<'a> {
hostname: &'a str,
}
self.integration("pia", "addKey", Some(&Form { hostname }))
.await
}
pub async fn geodata_files(self) -> Result<Vec<GeoFile>> {
self.get_object("geodata/files").await
}
pub async fn geodata_categories(
self,
file: &str,
query: &str,
offset: u32,
limit: u16,
) -> Result<GeoCategoryPage> {
#[derive(Serialize)]
struct Query<'a> {
file: &'a str,
q: &'a str,
offset: u32,
limit: u16,
}
let path = format!("{XRAY_ROOT}/geodata/categories");
let envelope = self
.client
.execute_query::<GeoCategoryPage, _>(
Method::GET,
&path,
&Query {
file,
q: query,
offset,
limit,
},
AuthenticationScope::PanelApi,
)
.await?;
required_object(self.client, Method::GET, &path, envelope)
}
pub async fn geodata_entries(
self,
file: &str,
code: &str,
query: &str,
offset: u32,
limit: u16,
) -> Result<GeoEntryPage> {
#[derive(Serialize)]
struct Query<'a> {
file: &'a str,
code: &'a str,
q: &'a str,
offset: u32,
limit: u16,
}
let path = format!("{XRAY_ROOT}/geodata/entries");
let envelope = self
.client
.execute_query::<GeoEntryPage, _>(
Method::GET,
&path,
&Query {
file,
code,
q: query,
offset,
limit,
},
AuthenticationScope::PanelApi,
)
.await?;
required_object(self.client, Method::GET, &path, envelope)
}
pub async fn validate_geodata_tokens(
self,
ip: bool,
tokens: &[impl AsRef<str>],
) -> Result<Vec<GeodataTokenIssue>> {
#[derive(Serialize)]
struct Form<'a> {
kind: &'a str,
tokens: String,
}
let tokens = tokens
.iter()
.map(AsRef::as_ref)
.collect::<Vec<_>>()
.join(",");
self.post_form_object(
"geodata/validate",
&Form {
kind: if ip { "ip" } else { "site" },
tokens,
},
)
.await
}
pub async fn reset_outbound_traffic(self, tag: &str) -> Result<()> {
#[derive(Serialize)]
struct Form<'a> {
tag: &'a str,
}
self.post_form_empty("resetOutboundsTraffic", &Form { tag })
.await
}
pub async fn test_outbound(
self,
outbound: &Value,
all_outbounds: Option<&[Value]>,
mode: OutboundTestMode,
) -> Result<OutboundTestResult> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Form<'a> {
outbound: &'a str,
all_outbounds: &'a str,
mode: &'a str,
}
let outbound = encode_json("test outbound", outbound)?;
let all_outbounds = encode_optional_json("test outbound dependencies", all_outbounds)?;
self.post_form_object(
"testOutbound",
&Form {
outbound: &outbound,
all_outbounds: &all_outbounds,
mode: mode.as_str(),
},
)
.await
}
pub async fn test_outbounds(
self,
outbounds: &[Value],
all_outbounds: Option<&[Value]>,
mode: OutboundTestMode,
) -> Result<Vec<OutboundTestResult>> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Form<'a> {
outbounds: &'a str,
all_outbounds: &'a str,
mode: &'a str,
}
let outbounds = encode_json("test outbounds", outbounds)?;
let all_outbounds = encode_optional_json("test outbound dependencies", all_outbounds)?;
self.post_form_object(
"testOutbounds",
&Form {
outbounds: &outbounds,
all_outbounds: &all_outbounds,
mode: mode.as_str(),
},
)
.await
}
pub async fn balancer_status(
self,
tags: &[impl AsRef<str>],
) -> Result<BTreeMap<String, BalancerStatus>> {
#[derive(Serialize)]
struct Form {
tags: String,
}
let tags = tags.iter().map(AsRef::as_ref).collect::<Vec<_>>().join(",");
self.post_form_object("balancerStatus", &Form { tags })
.await
}
pub async fn set_balancer_override(self, tag: &str, target: &str) -> Result<()> {
#[derive(Serialize)]
struct Form<'a> {
tag: &'a str,
target: &'a str,
}
self.post_form_empty("balancerOverride", &Form { tag, target })
.await
}
pub async fn test_route(self, request: &RouteTestRequest) -> Result<RouteTestResult> {
self.post_form_object("routeTest", request).await
}
pub async fn outbound_subscriptions(self) -> Result<Vec<OutboundSubscription>> {
self.get_object("outbound-subs").await
}
pub async fn create_outbound_subscription(
self,
input: &OutboundSubscriptionInput,
) -> Result<OutboundSubscription> {
self.post_form_object("outbound-subs", input).await
}
pub async fn refresh_outbound_subscription(self, id: i64) -> Result<OutboundDocuments> {
self.post_form_object(&format!("outbound-subs/{id}/refresh"), &())
.await
}
pub async fn move_outbound_subscription(self, id: i64, direction: MoveDirection) -> Result<()> {
#[derive(Serialize)]
struct Form<'a> {
dir: &'a str,
}
self.post_form_empty(
&format!("outbound-subs/{id}/move"),
&Form {
dir: direction.as_str(),
},
)
.await
}
pub async fn update_outbound_subscription(
self,
id: i64,
input: &OutboundSubscriptionInput,
) -> Result<()> {
self.post_form_empty(&format!("outbound-subs/{id}"), input)
.await
}
pub async fn delete_outbound_subscription(self, id: i64) -> Result<()> {
let path = format!("{XRAY_ROOT}/outbound-subs/{id}");
self.client
.execute::<Value, ()>(Method::DELETE, &path, None, AuthenticationScope::PanelApi)
.await?;
Ok(())
}
pub async fn delete_outbound_subscription_via_post(self, id: i64) -> Result<()> {
self.post_form_empty(&format!("outbound-subs/{id}/del"), &())
.await
}
pub async fn parse_outbound_subscription(
self,
url: &str,
allow_private: bool,
allow_insecure: bool,
) -> Result<OutboundDocuments> {
self.parse_outbound_subscription_with_user_agent(url, allow_private, allow_insecure, "")
.await
}
pub async fn parse_outbound_subscription_with_user_agent(
self,
url: &str,
allow_private: bool,
allow_insecure: bool,
user_agent: &str,
) -> Result<OutboundDocuments> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Form<'a> {
url: &'a str,
allow_private: bool,
allow_insecure: bool,
user_agent: &'a str,
}
self.post_form_object(
"outbound-subs/parse",
&Form {
url,
allow_private,
allow_insecure,
user_agent,
},
)
.await
}
async fn integration<T, B>(self, family: &str, action: &str, form: Option<&B>) -> Result<T>
where
T: DeserializeOwned,
B: Serialize + ?Sized,
{
let path = format!("{XRAY_ROOT}/{family}/{}", segment(action));
let envelope = match form {
Some(form) => {
self.client
.execute_form(Method::POST, &path, form, AuthenticationScope::PanelApi)
.await?
}
None => {
self.client
.execute::<T, ()>(Method::POST, &path, None, AuthenticationScope::PanelApi)
.await?
}
};
required_object(self.client, Method::POST, &path, envelope)
}
async fn integration_empty<B>(self, family: &str, action: &str, form: Option<&B>) -> Result<()>
where
B: Serialize + ?Sized,
{
let path = format!("{XRAY_ROOT}/{family}/{}", segment(action));
match form {
Some(form) => {
self.client
.execute_form::<Value, _>(
Method::POST,
&path,
form,
AuthenticationScope::PanelApi,
)
.await?;
}
None => {
self.client
.execute::<Value, ()>(Method::POST, &path, None, AuthenticationScope::PanelApi)
.await?;
}
}
Ok(())
}
async fn get_object<T>(self, suffix: &str) -> Result<T>
where
T: DeserializeOwned,
{
let path = format!("{XRAY_ROOT}/{suffix}");
let envelope = self
.client
.execute::<T, ()>(Method::GET, &path, None, AuthenticationScope::PanelApi)
.await?;
required_object(self.client, Method::GET, &path, envelope)
}
async fn post_form_object<T, B>(self, suffix: &str, form: &B) -> Result<T>
where
T: DeserializeOwned,
B: Serialize + ?Sized,
{
let path = format!("{XRAY_ROOT}/{suffix}");
let envelope = self
.client
.execute_form(Method::POST, &path, form, AuthenticationScope::PanelApi)
.await?;
required_object(self.client, Method::POST, &path, envelope)
}
async fn post_form_empty<B>(self, suffix: &str, form: &B) -> Result<()>
where
B: Serialize + ?Sized,
{
let path = format!("{XRAY_ROOT}/{suffix}");
self.client
.execute_form::<Value, _>(Method::POST, &path, form, AuthenticationScope::PanelApi)
.await?;
Ok(())
}
}
fn required_object<T>(
client: &Client,
method: Method,
path: &str,
envelope: ApiResponse<T>,
) -> Result<T> {
let url = client.endpoint(path)?;
envelope.obj.ok_or_else(|| Error::MissingObject {
method,
url: Box::new(url),
})
}
fn encode_json<T: Serialize + ?Sized>(operation: &'static str, value: &T) -> Result<String> {
serde_json::to_string(value).map_err(|source| Error::Encode { operation, source })
}
fn encode_optional_json<T: Serialize + ?Sized>(
operation: &'static str,
value: Option<&T>,
) -> Result<String> {
value.map_or_else(|| Ok(String::new()), |value| encode_json(operation, value))
}
fn segment(value: &str) -> String {
utf8_percent_encode(value, NON_ALPHANUMERIC).to_string()
}