novel_api/sfacg/
utils.rs

1use std::time::{SystemTime, UNIX_EPOCH};
2
3use hex_simd::AsciiCase;
4use http::HeaderValue;
5use reqwest::Response;
6use serde::Serialize;
7use tokio::sync::OnceCell;
8use url::Url;
9use uuid::Uuid;
10
11use crate::{Error, HTTPClient, NovelDB, SfacgClient};
12
13include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
14
15impl SfacgClient {
16    const APP_NAME: &'static str = "sfacg";
17
18    const HOST: &'static str = "https://api.sfacg.com";
19    const USER_AGENT: &'static str = "boluobao/5.1.56(iOS;18.3.2)/appStore/{}/appStore";
20    const USER_AGENT_RSS: &'static str = "SFReader/5.1.56 (iPhone; iOS 18.3.2; Scale/3.00)";
21
22    const USERNAME: &'static str = "apiuser";
23    const PASSWORD: &'static str = "3s#1-yt6e*Acv@qer";
24
25    const SALT: &'static str = "a@Lk7Tf4gh8TUPoX";
26
27    /// Create a sfacg client
28    pub async fn new() -> Result<Self, Error> {
29        Ok(Self {
30            proxy: None,
31            no_proxy: false,
32            cert_path: None,
33            client: OnceCell::new(),
34            client_rss: OnceCell::new(),
35            db: OnceCell::new(),
36        })
37    }
38
39    pub(crate) async fn db(&self) -> Result<&NovelDB, Error> {
40        self.db
41            .get_or_try_init(|| async { NovelDB::new(SfacgClient::APP_NAME).await })
42            .await
43    }
44
45    pub(crate) async fn client(&self) -> Result<&HTTPClient, Error> {
46        self.client
47            .get_or_try_init(|| async {
48                let device_token = crate::uid();
49                let user_agent = SfacgClient::USER_AGENT.replace("{}", device_token);
50
51                HTTPClient::builder()
52                    .app_name(SfacgClient::APP_NAME)
53                    .accept(HeaderValue::from_static(
54                        "application/vnd.sfacg.api+json;version=1",
55                    ))
56                    .accept_language(HeaderValue::from_static("zh-Hans-CN;q=1"))
57                    .cookie(true)
58                    .user_agent(user_agent)
59                    .maybe_proxy(self.proxy.clone())
60                    .no_proxy(self.no_proxy)
61                    .maybe_cert_path(self.cert_path.clone())
62                    .build()
63                    .await
64            })
65            .await
66    }
67
68    pub(crate) async fn client_rss(&self) -> Result<&HTTPClient, Error> {
69        self.client_rss
70            .get_or_try_init(|| async {
71                HTTPClient::builder()
72                    .app_name(SfacgClient::APP_NAME)
73                    .accept(HeaderValue::from_static("image/*,*/*;q=0.8"))
74                    .accept_language(HeaderValue::from_static("zh-CN,zh-Hans;q=0.9"))
75                    .user_agent(SfacgClient::USER_AGENT_RSS.to_string())
76                    .maybe_proxy(self.proxy.clone())
77                    .no_proxy(self.no_proxy)
78                    .maybe_cert_path(self.cert_path.clone())
79                    .build()
80                    .await
81            })
82            .await
83    }
84
85    pub(crate) async fn get<T>(&self, url: T) -> Result<Response, Error>
86    where
87        T: AsRef<str>,
88    {
89        Ok(self
90            .client()
91            .await?
92            .get(SfacgClient::HOST.to_string() + url.as_ref())
93            .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
94            .header("sfsecurity", self.sf_security()?)
95            .send()
96            .await?)
97    }
98
99    pub(crate) async fn get_query<T, E>(&self, url: T, query: E) -> Result<Response, Error>
100    where
101        T: AsRef<str>,
102        E: Serialize,
103    {
104        let mut count = 0;
105
106        let response = loop {
107            let response = self
108                .client()
109                .await?
110                .get(SfacgClient::HOST.to_string() + url.as_ref())
111                .query(&query)
112                .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
113                .header("sfsecurity", self.sf_security()?)
114                .send()
115                .await;
116
117            if let Ok(response) = response {
118                break response;
119            } else {
120                tracing::info!(
121                    "HTTP request failed: `{}`, retry, number of times: `{}`",
122                    response.as_ref().unwrap_err(),
123                    count + 1
124                );
125
126                count += 1;
127                if count > 3 {
128                    response?;
129                }
130            }
131        };
132
133        Ok(response)
134    }
135
136    pub(crate) async fn post<T, E>(&self, url: T, json: E) -> Result<Response, Error>
137    where
138        T: AsRef<str>,
139        E: Serialize,
140    {
141        Ok(self
142            .client()
143            .await?
144            .post(SfacgClient::HOST.to_string() + url.as_ref())
145            .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
146            .header("sfsecurity", self.sf_security()?)
147            .json(&json)
148            .send()
149            .await?)
150    }
151
152    pub(crate) async fn put<T, E>(&self, url: T, json: E) -> Result<Response, Error>
153    where
154        T: AsRef<str>,
155        E: Serialize,
156    {
157        Ok(self
158            .client()
159            .await?
160            .put(SfacgClient::HOST.to_string() + url.as_ref())
161            .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
162            .header("sfsecurity", self.sf_security()?)
163            .json(&json)
164            .send()
165            .await?)
166    }
167
168    pub(crate) async fn get_rss(&self, url: &Url) -> Result<Response, Error> {
169        let response = self.client_rss().await?.get(url.clone()).send().await?;
170        crate::check_status(response.status(), format!("HTTP request failed: `{url}`"))?;
171
172        Ok(response)
173    }
174
175    fn sf_security(&self) -> Result<String, Error> {
176        let uuid = Uuid::new_v4();
177        let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
178        let device_token = crate::uid();
179
180        let sign = crate::md5_hex(
181            format!("{uuid}{timestamp}{device_token}{}", SfacgClient::SALT),
182            AsciiCase::Upper,
183        );
184
185        Ok(format!(
186            "nonce={uuid}&timestamp={timestamp}&devicetoken={device_token}&sign={sign}"
187        ))
188    }
189
190    pub(crate) fn convert(content: String) -> String {
191        let mut result = String::with_capacity(content.len());
192
193        for c in content.chars() {
194            result.push(*CHARACTER_MAPPER.get(&c).unwrap_or(&c));
195        }
196
197        result
198    }
199}