1use secrecy::{ExposeSecret, SecretString};
2use serde::Serialize;
3use serde_json::json;
4use sha2::{Digest, Sha256};
5use std::time::Duration;
6use url::Url;
7
8use crate::APP_USER_AGENT;
9use crate::auth::{Auth, login};
10use crate::error::RsError;
11
12mod private {
14 use secrecy::SecretString;
15
16 pub struct NoUrl;
17 pub struct WithUrl(pub(crate) url::Url);
18 pub struct NoAuth;
19 pub struct WithUserKey {
20 pub(crate) user: String,
21 pub(crate) key: SecretString,
22 }
23 pub struct WithSessionKey {
24 pub(crate) user: String,
25 pub(crate) password: SecretString,
26 }
27}
28
29#[derive(Serialize)]
30pub(crate) struct ApiRequest<'a, P: Serialize> {
31 pub(crate) user: &'a str,
32 #[serde(rename = "function")]
33 pub(crate) function: &'a str,
34 #[serde(flatten)]
35 pub(crate) params: P,
36}
37
38pub(crate) fn build_query<P: Serialize>(params: &P) -> Result<String, RsError> {
39 serde_qs::Config::new()
40 .use_form_encoding(true)
41 .serialize_string(params)
42 .map_err(|e| RsError::Other(format!("Failed to serialize request: {}", e)))
43}
44
45#[derive(Debug)]
56pub struct Client {
57 base_url: Url,
58 auth: Auth,
59 client: reqwest::Client,
60}
61
62impl Client {
63 #[must_use]
64 pub fn builder() -> ClientBuilder<private::NoUrl, private::NoAuth> {
65 ClientBuilder {
66 base_url: private::NoUrl,
67 auth: private::NoAuth,
68 }
69 }
70
71 pub(crate) async fn send_request<P>(
72 &self,
73 function: &str,
74 method: reqwest::Method,
75 params: P,
76 ) -> Result<serde_json::Value, RsError>
77 where
78 P: Serialize,
79 {
80 let (user, key, authmode) = match &self.auth {
81 Auth::UserKey { user, key } => (user, key.expose_secret(), "userkey"),
82 Auth::SessionKey { user, key } => (user, key.expose_secret(), "sessionkey"),
83 };
84
85 let req = ApiRequest {
87 user,
88 function,
89 params,
90 };
91 let query = build_query(&req)?;
92 let signature = sign(key, &query);
93
94 let response = match method {
95 reqwest::Method::GET => {
96 let full_url = format!(
97 "{}api/?{}&sign={}&authmode={}",
98 self.base_url, query, signature, authmode
99 );
100 self.client.get(&full_url).send().await
101 }
102 reqwest::Method::POST => {
103 let full_url = format!("{}api/", self.base_url);
104 self.client
105 .post(&full_url)
106 .form(&[
107 ("user", user.clone()),
108 ("query", query),
109 ("sign", signature),
110 ("authmode", authmode.to_string()),
111 ])
112 .send()
113 .await
114 }
115 _ => return Err(RsError::Other("Unsupported HTTP method".into())),
116 }
117 .map_err(RsError::Http)?;
118
119 if !response.status().is_success() {
121 return Err(RsError::Api {
122 status: response.status().as_u16(),
123 message: response.text().await.unwrap_or_default(),
124 });
125 }
126
127 let text = response.text().await.map_err(RsError::Http)?;
128 let trimmed = text.trim();
129
130 if trimmed.eq_ignore_ascii_case("false") {
132 return Err(RsError::OperationFailed);
133 }
134
135 if let Some(msg) = trimmed.strip_prefix("FAILED:") {
137 return Err(RsError::Api {
138 status: 400,
139 message: msg.trim().to_string(),
140 });
141 }
142
143 let json: serde_json::Value = serde_json::from_str(trimmed)
146 .unwrap_or_else(|_| serde_json::Value::String(trimmed.to_string()));
147
148 Ok(json)
149 }
150
151 pub(crate) async fn send_multipart_request<P>(
152 &self,
153 function: &str,
154 params: P,
155 file: &std::path::Path,
156 ) -> Result<serde_json::Value, RsError>
157 where
158 P: Serialize,
159 {
160 let (user, key, authmode) = match &self.auth {
161 Auth::UserKey { user, key } => (user, key.expose_secret(), "userkey"),
162 Auth::SessionKey { user, key } => (user, key.expose_secret(), "sessionkey"),
163 };
164
165 let req = ApiRequest {
167 user,
168 function,
169 params,
170 };
171 let query = build_query(&req)?;
172 let signature = sign(key, &query);
173
174 let full_url = format!("{}api/", self.base_url);
175
176 let response = self
177 .client
178 .post(&full_url)
179 .multipart(
180 reqwest::multipart::Form::new()
181 .text("user", user.clone())
182 .text("query", query)
183 .text("sign", signature)
184 .text("authmode", authmode.to_string())
185 .file("file", file)
186 .await
187 .map_err(|e| RsError::Other(format!("Failed to read file: {}", e)))?,
188 )
189 .send()
190 .await
191 .map_err(RsError::Http)?;
192
193 if !response.status().is_success() {
194 return Err(RsError::Api {
195 status: response.status().as_u16(),
196 message: response.text().await.unwrap_or_default(),
197 });
198 }
199
200 let text = response.text().await.map_err(RsError::Http)?;
201
202 Ok(json!(text))
203 }
204
205 pub fn search(&self) -> crate::api::search::SearchApi<'_> {
207 crate::api::search::SearchApi::new(self)
208 }
209 pub fn system(&self) -> crate::api::system::SystemApi<'_> {
210 crate::api::system::SystemApi::new(self)
211 }
212 pub fn message(&self) -> crate::api::message::MessageApi<'_> {
213 crate::api::message::MessageApi::new(self)
214 }
215 pub fn metadata(&self) -> crate::api::metadata::MetadataApi<'_> {
216 crate::api::metadata::MetadataApi::new(self)
217 }
218 pub fn user(&self) -> crate::api::user::UserApi<'_> {
219 crate::api::user::UserApi::new(self)
220 }
221 pub fn collection(&self) -> crate::api::collection::CollectionApi<'_> {
222 crate::api::collection::CollectionApi::new(self)
223 }
224 pub fn resource(&self) -> crate::api::resource::ResourceApi<'_> {
225 crate::api::resource::ResourceApi::new(self)
226 }
227}
228
229pub struct ClientBuilder<U = private::NoUrl, A = private::NoAuth> {
230 base_url: U,
231 auth: A,
232}
233
234impl<A> ClientBuilder<private::NoUrl, A> {
235 pub fn base_url(
236 self,
237 url: impl Into<String>,
238 ) -> Result<ClientBuilder<private::WithUrl, A>, RsError> {
239 let url = url.into();
240 let parsed_url = Url::parse(&url).map_err(|e| RsError::Other(e.to_string()))?;
241
242 Ok(ClientBuilder {
243 base_url: private::WithUrl(parsed_url),
244 auth: self.auth,
245 })
246 }
247}
248
249impl<U> ClientBuilder<U, private::NoAuth> {
250 pub fn user_key(
251 self,
252 user: impl Into<String>,
253 key: impl Into<String>,
254 ) -> ClientBuilder<U, private::WithUserKey> {
255 ClientBuilder {
256 base_url: self.base_url,
257 auth: private::WithUserKey {
258 user: user.into(),
259 key: SecretString::from(key.into()),
260 },
261 }
262 }
263
264 pub fn session_key(
265 self,
266 user: impl Into<String>,
267 password: impl Into<String>,
268 ) -> ClientBuilder<U, private::WithSessionKey> {
269 ClientBuilder {
270 base_url: self.base_url,
271 auth: private::WithSessionKey {
272 user: user.into(),
273 password: SecretString::from(password.into()),
274 },
275 }
276 }
277}
278
279impl ClientBuilder<private::WithUrl, private::WithSessionKey> {
280 pub async fn build(self) -> Result<Client, RsError> {
281 let http = make_client()?;
282 let session_key = login(
283 &http,
284 &self.base_url.0,
285 &self.auth.user,
286 self.auth.password.expose_secret(),
287 )
288 .await?;
289 let auth = Auth::SessionKey {
290 user: self.auth.user,
291 key: SecretString::from(session_key),
292 };
293
294 Ok(Client {
295 base_url: self.base_url.0,
296 auth,
297 client: http,
298 })
299 }
300}
301
302impl ClientBuilder<private::WithUrl, private::WithUserKey> {
303 pub async fn build(self) -> Result<Client, RsError> {
304 let http = make_client()?;
305 let auth = Auth::UserKey {
306 user: self.auth.user,
307 key: self.auth.key,
308 };
309
310 Ok(Client {
311 base_url: self.base_url.0,
312 auth,
313 client: http,
314 })
315 }
316}
317
318fn sign(key: &str, query: &str) -> String {
319 let mut hasher = Sha256::new();
320 hasher.update(key.as_bytes());
321 hasher.update(query.as_bytes());
322 hex::encode(hasher.finalize())
323}
324
325fn make_client() -> Result<reqwest::Client, RsError> {
326 Ok(reqwest::Client::builder()
327 .timeout(Duration::from_secs(30))
328 .connect_timeout(Duration::from_secs(10))
329 .user_agent(APP_USER_AGENT)
330 .build()?)
331}