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