1use serde::{Deserialize, Deserializer, Serialize};
2
3fn deserialize_null_default<'de, D, T>(de: D) -> Result<T, D::Error>
4where
5 D: Deserializer<'de>,
6 T: Default + Deserialize<'de>,
7{
8 let opt = Option::<T>::deserialize(de)?;
9 Ok(opt.unwrap_or_default())
10}
11
12fn deserialize_flex_i64<'de, D>(de: D) -> Result<i64, D::Error>
15where
16 D: Deserializer<'de>,
17{
18 use serde::de::Error;
19 let v = serde_json::Value::deserialize(de)?;
20 match v {
21 serde_json::Value::Null => Ok(0),
22 serde_json::Value::Number(n) => n
23 .as_i64()
24 .or_else(|| n.as_f64().map(|f| f as i64))
25 .ok_or_else(|| D::Error::custom("number out of range for i64")),
26 serde_json::Value::String(s) => {
27 if s.is_empty() {
28 Ok(0)
29 } else {
30 s.parse::<i64>().map_err(D::Error::custom)
31 }
32 }
33 other => Err(D::Error::custom(format!(
34 "expected i64-compatible value, got {}",
35 other
36 ))),
37 }
38}
39
40fn serialize_i64<S>(v: &i64, ser: S) -> Result<S::Ok, S::Error>
41where
42 S: serde::Serializer,
43{
44 ser.serialize_i64(*v)
45}
46
47#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "lowercase")]
49pub enum Protocol {
50 VMess,
51 VLess,
52 Trojan,
53 Shadowsocks,
54 Hysteria,
55 Hysteria2,
56 WireGuard,
57 HTTP,
58 Mixed,
59 #[serde(other)]
60 #[default]
61 Unknown,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct ClientTraffic {
67 pub id: i64,
68 pub inbound_id: i64,
69 pub enable: bool,
70 pub email: String,
71 #[serde(default)]
72 pub uuid: String,
73 #[serde(default)]
74 pub sub_id: String,
75 pub up: i64,
76 pub down: i64,
77 #[serde(default)]
78 pub all_time: i64,
79 pub expiry_time: i64,
80 pub total: i64,
81 #[serde(default)]
82 pub reset: i32,
83 #[serde(default)]
84 pub last_online: i64,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, Default)]
88#[serde(rename_all = "camelCase")]
89pub struct Inbound {
90 #[serde(default)]
91 pub id: i64,
92 pub up: i64,
93 pub down: i64,
94 pub total: i64,
95 #[serde(default)]
96 pub all_time: i64,
97 pub remark: String,
98 pub enable: bool,
99 pub expiry_time: i64,
100 #[serde(default)]
101 pub traffic_reset: String,
102 #[serde(default)]
103 pub last_traffic_reset_time: i64,
104 #[serde(default, deserialize_with = "deserialize_null_default")]
105 pub client_stats: Vec<ClientTraffic>,
106 pub listen: String,
107 pub port: u16,
108 pub protocol: Protocol,
109 pub settings: serde_json::Value,
110 pub stream_settings: serde_json::Value,
111 pub tag: String,
112 pub sniffing: serde_json::Value,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, Default)]
116#[serde(rename_all = "camelCase")]
117pub struct InboundClient {
118 #[serde(default, skip_serializing_if = "String::is_empty")]
119 pub id: String,
120 pub email: String,
121 pub enable: bool,
122 #[serde(default, skip_serializing_if = "String::is_empty")]
123 pub flow: String,
124 #[serde(default, skip_serializing_if = "String::is_empty")]
125 pub password: String,
126 #[serde(default, skip_serializing_if = "String::is_empty")]
127 pub security: String,
128 #[serde(default)]
129 pub limit_ip: i32,
130 #[serde(default, rename = "totalGB")]
131 pub total_gb: i64,
132 #[serde(default)]
133 pub expiry_time: i64,
134 #[serde(
135 default,
136 deserialize_with = "deserialize_flex_i64",
137 serialize_with = "serialize_i64"
138 )]
139 pub tg_id: i64,
140 #[serde(default, skip_serializing_if = "String::is_empty")]
141 pub sub_id: String,
142 #[serde(default, skip_serializing_if = "String::is_empty")]
143 pub comment: String,
144 pub reset: i32,
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn protocol_deserializes() {
153 let p: Protocol = serde_json::from_str(r#""vmess""#).unwrap();
154 assert_eq!(p, Protocol::VMess);
155 let p: Protocol = serde_json::from_str(r#""vless""#).unwrap();
156 assert_eq!(p, Protocol::VLess);
157 }
158
159 #[test]
160 fn protocol_unknown_variant() {
161 let p: Protocol = serde_json::from_str(r#""socks5""#).unwrap();
162 assert_eq!(p, Protocol::Unknown);
163 }
164
165 #[test]
166 fn inbound_with_null_client_stats() {
167 let raw = r#"{
169 "id":1,"up":0,"down":0,"total":0,"remark":"x","enable":true,
170 "expiryTime":0,"listen":"","port":80,"protocol":"vless",
171 "settings":"{}","streamSettings":"{}","tag":"i","sniffing":"{}",
172 "clientStats":null
173 }"#;
174 let inb: Inbound = serde_json::from_str(raw).unwrap();
175 assert!(inb.client_stats.is_empty());
176 }
177
178 #[test]
179 fn inbound_client_with_newer_fields() {
180 let raw = r#"{
183 "id":"abc","email":"u@example.com","enable":true,"flow":"",
184 "limitIp":1,"totalGB":1073741824,"expiryTime":-604800000,
185 "tgId":"77313385","subId":"x","comment":"hi",
186 "created_at":1777667608000,"updated_at":1777667608000,
187 "reset":0
188 }"#;
189 let c: InboundClient = serde_json::from_str(raw).unwrap();
190 assert_eq!(c.total_gb, 1073741824);
191 assert_eq!(c.tg_id, 77313385);
192 assert_eq!(c.comment, "hi");
193 }
194
195 #[test]
196 fn inbound_client_tg_id_as_int_or_null() {
197 let raw_int = r#"{"id":"a","email":"e","enable":true,"limitIp":0,"totalGB":0,"expiryTime":0,"tgId":42,"reset":0}"#;
198 let c: InboundClient = serde_json::from_str(raw_int).unwrap();
199 assert_eq!(c.tg_id, 42);
200
201 let raw_null = r#"{"id":"a","email":"e","enable":true,"limitIp":0,"totalGB":0,"expiryTime":0,"tgId":null,"reset":0}"#;
202 let c: InboundClient = serde_json::from_str(raw_null).unwrap();
203 assert_eq!(c.tg_id, 0);
204 }
205
206 #[test]
207 fn inbound_deserializes() {
208 let raw = r#"{
209 "id":1,"up":0,"down":0,"total":0,"remark":"test",
210 "enable":true,"expiryTime":0,"listen":"","port":443,
211 "protocol":"vless","settings":{},"streamSettings":{},
212 "tag":"inbound-443","sniffing":{},"clientStats":[]
213 }"#;
214 let inbound: Inbound = serde_json::from_str(raw).unwrap();
215 assert_eq!(inbound.id, 1);
216 assert_eq!(inbound.protocol, Protocol::VLess);
217 assert_eq!(inbound.port, 443);
218 }
219}