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.16(iOS;26.0.1)/appStore/{}/appStore";
21    const USER_AGENT_RSS: &'static str = "SFReader/5.2.16 (iPhone; iOS 26.0.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();
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 response = self
111            .client()
112            .await?
113            .get(SfacgClient::HOST.to_string() + url.as_ref())
114            .query(&query)
115            .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
116            .header("sfsecurity", self.sf_security()?)
117            .send()
118            .await?;
119
120        Ok(sonic_rs::from_slice(&response.bytes().await?)?)
121    }
122
123    pub(crate) async fn post<T, E, R>(&self, url: T, json: E) -> Result<R, Error>
124    where
125        T: AsRef<str>,
126        E: Serialize,
127        R: DeserializeOwned,
128    {
129        let response = self
130            .client()
131            .await?
132            .post(SfacgClient::HOST.to_string() + url.as_ref())
133            .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
134            .header("sfsecurity", self.sf_security()?)
135            .json(&json)
136            .send()
137            .await?;
138
139        Ok(sonic_rs::from_slice(&response.bytes().await?)?)
140    }
141
142    pub(crate) async fn put<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            .put(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 get_rss(&self, url: &Url) -> Result<Response, Error> {
162        let response = self.client_rss().await?.get(url.clone()).send().await?;
163        crate::check_status(response.status(), format!("HTTP request failed: `{url}`"))?;
164
165        Ok(response)
166    }
167
168    fn sf_security(&self) -> Result<String, Error> {
169        let uuid = Uuid::new_v4();
170        let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
171        let device_token = crate::uid();
172
173        let sign = crate::md5_hex(
174            format!("{uuid}{timestamp}{device_token}{}", SfacgClient::SALT),
175            AsciiCase::Upper,
176        );
177
178        Ok(format!(
179            "nonce={uuid}&timestamp={timestamp}&devicetoken={device_token}&sign={sign}"
180        ))
181    }
182
183    pub(crate) fn convert(content: String) -> String {
184        let mut result = String::with_capacity(content.len());
185
186        for c in content.chars() {
187            result.push(*CHARACTER_MAPPER.get(&c).unwrap_or(&c));
188        }
189
190        result
191    }
192}