Skip to main content

fishpi_sdk/utils/
mod.rs

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
182/// 构造带查询参数的相对 HTTP 路径,自动进行 query 编码
183pub 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    /// 是否成功
201    pub success: bool,
202    /// 执行结果或错误信息
203    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}