1pub mod error;
2
3use crate::utils::error::Error;
4
5use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
6use reqwest::{Client, Method, StatusCode, multipart};
7use serde_json::Value;
8use std::collections::HashMap;
9use std::time::Duration;
10use tokio::time::sleep;
11use url::form_urlencoded::Serializer;
12
13lazy_static::lazy_static! {
14 static ref CLIENT: Client = Client::new();
15}
16
17const DOMAIN: &str = "fishpi.cn";
18
19pub async fn get(url: &str) -> Result<Value, Error> {
20 request("GET", url, None, None).await
21}
22
23pub async fn put(url: &str, data: Option<Value>) -> Result<Value, Error> {
24 request("PUT", url, None, data).await
25}
26
27pub async fn get_text(url: &str) -> Result<String, Error> {
28 let full_url = format!("https://{}/{}", DOMAIN, url.trim_start_matches('/'));
29
30 let resp = CLIENT
31 .get(&full_url)
32 .header(
33 "User-Agent",
34 "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36",
35 )
36 .header("Referer", &format!("https://{}/", DOMAIN))
37 .send()
38 .await
39 .map_err(|e| Error::Request(Box::new(e)))?;
40
41 if !resp.status().is_success() {
42 return Err(Error::Request(
43 format!("HTTP error: {}", resp.status()).into(),
44 ));
45 }
46
47 resp.text().await.map_err(|e| Error::Request(Box::new(e)))
48}
49
50pub async fn get_with_key(url: &str, api_key: &str) -> Result<Value, Error> {
51 let url_with_key = build_http_path(url, &[("apiKey", api_key.to_string())]);
52 request("GET", &url_with_key, None, None).await
53}
54
55pub async fn post(url: &str, data: Option<Value>) -> Result<Value, Error> {
56 request("POST", url, None, data).await
57}
58
59pub async fn delete(url: &str, data: Option<Value>) -> Result<Value, Error> {
60 request("DELETE", url, None, data).await
61}
62
63pub async fn upload_files(url: &str, files: Vec<String>, api_key: &str) -> Result<Value, Error> {
64 let mut form = multipart::Form::new();
65
66 for file_path in files {
67 if !std::path::Path::new(&file_path).exists() {
68 return Err(Error::Api(format!("File not exist: {}", file_path)));
69 }
70 let file_content = tokio::fs::read(&file_path)
71 .await
72 .map_err(|e| Error::Api(format!("Failed to read file {}: {}", file_path, e)))?;
73 let file_name = std::path::Path::new(&file_path)
74 .file_name()
75 .and_then(|n| n.to_str())
76 .unwrap_or("file")
77 .to_string();
78 form = form.part(
79 "file[]",
80 multipart::Part::stream(file_content).file_name(file_name),
81 );
82 }
83
84 form = form.text("apiKey", api_key.to_string());
85
86 let response = CLIENT
87 .post(url)
88 .multipart(form)
89 .send()
90 .await
91 .map_err(|e| Error::Api(format!("Request failed: {}", e)))?;
92
93 let rsp: Value = response
94 .json()
95 .await
96 .map_err(|e| Error::Api(format!("Failed to parse response: {}", e)))?;
97
98 Ok(rsp)
99}
100
101async fn request(
102 method: &str,
103 url: &str,
104 headers: Option<HashMap<String, String>>,
105 data: Option<Value>,
106) -> Result<Value, Error> {
107 let full_url = format!("https://{}/{}", DOMAIN, url.trim_start_matches('/'));
108
109 let method = method
110 .parse::<Method>()
111 .map_err(|e| Error::Request(Box::new(e)))?;
112 let extra_headers = if let Some(headers) = headers {
113 Some(
114 headers
115 .into_iter()
116 .map(|(k, v)| {
117 let name = HeaderName::from_bytes(k.as_bytes())
118 .map_err(|e| Error::Request(Box::new(e)))?;
119 let value =
120 HeaderValue::from_str(&v).map_err(|e| Error::Request(Box::new(e)))?;
121 Ok((name, value))
122 })
123 .collect::<Result<HeaderMap, Error>>()?,
124 )
125 } else {
126 None
127 };
128
129 let max_retries = 2;
130 let mut attempt = 0;
131
132 loop {
133 let mut req = CLIENT
134 .request(method.clone(), &full_url)
135 .header(
136 "User-Agent",
137 "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36",
138 )
139 .header("Referer", &format!("https://{}/", DOMAIN));
140
141 if let Some(map) = extra_headers.clone() {
142 req = req.headers(map);
143 }
144
145 if let Some(body) = data.clone() {
146 req = req.json(&body);
147 }
148
149 let resp = match req.send().await {
150 Ok(resp) => resp,
151 Err(err) => {
152 if attempt < max_retries {
153 let wait_ms = 300 * (attempt + 1);
154 sleep(Duration::from_millis(wait_ms as u64)).await;
155 attempt += 1;
156 continue;
157 }
158 return Err(Error::Request(Box::new(err)));
159 }
160 };
161
162 if resp.status().is_success() {
163 return resp
164 .json::<Value>()
165 .await
166 .map_err(|e| Error::Request(Box::new(e)));
167 }
168
169 if resp.status() == StatusCode::SERVICE_UNAVAILABLE && attempt < max_retries {
170 let wait_ms = 300 * (attempt + 1);
171 sleep(Duration::from_millis(wait_ms as u64)).await;
172 attempt += 1;
173 continue;
174 }
175
176 return Err(Error::Request(
177 format!("HTTP error: {}", resp.status()).into(),
178 ));
179 }
180}
181
182pub fn build_http_path(path: &str, params: &[(&str, String)]) -> String {
184 if params.is_empty() {
185 return path.to_string();
186 }
187
188 let mut serializer = Serializer::new(String::new());
189 for (k, v) in params {
190 serializer.append_pair(k, v);
191 }
192 let query = serializer.finish();
193
194 format!("{}?{}", path, query)
195}
196
197#[derive(Clone, Debug)]
198#[allow(non_snake_case)]
199pub struct ResponseResult {
200 pub success: bool,
202 pub msg: String,
204}
205
206impl ResponseResult {
207 pub fn from_value(data: &Value) -> Result<Self, Error> {
208 Ok(ResponseResult {
209 success: data.get("code").and_then(|c| c.as_i64()).unwrap_or(0) == 0,
210 msg: data["msg"].as_str().unwrap_or("").to_string(),
211 })
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::build_http_path;
218
219 #[test]
220 fn build_http_path_encodes_query() {
221 let p = build_http_path(
222 "chat/get-message",
223 &[
224 ("apiKey", "token a+b".to_string()),
225 ("toUser", "alice/bob".to_string()),
226 ],
227 );
228
229 assert_eq!(p, "chat/get-message?apiKey=token+a%2Bb&toUser=alice%2Fbob");
230 }
231}