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.1.76(iOS;18.5)/appStore/{}/appStore";
21    const USER_AGENT_RSS: &'static str = "SFReader/5.1.76 (iPhone; iOS 18.5; 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                    .build()
64                    .await
65            })
66            .await
67    }
68
69    pub(crate) async fn client_rss(&self) -> Result<&HTTPClient, Error> {
70        self.client_rss
71            .get_or_try_init(|| async {
72                HTTPClient::builder()
73                    .app_name(SfacgClient::APP_NAME)
74                    .accept(HeaderValue::from_static("image/*,*/*;q=0.8"))
75                    .accept_language(HeaderValue::from_static("zh-CN,zh-Hans;q=0.9"))
76                    .user_agent(SfacgClient::USER_AGENT_RSS.to_string())
77                    .maybe_proxy(self.proxy.clone())
78                    .no_proxy(self.no_proxy)
79                    .maybe_cert_path(self.cert_path.clone())
80                    .build()
81                    .await
82            })
83            .await
84    }
85
86    pub(crate) async fn get<T, R>(&self, url: T) -> Result<R, Error>
87    where
88        T: AsRef<str>,
89        R: DeserializeOwned,
90    {
91        let response = self
92            .client()
93            .await?
94            .get(SfacgClient::HOST.to_string() + url.as_ref())
95            .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
96            .header("sfsecurity", self.sf_security()?)
97            .send()
98            .await?;
99
100        Ok(sonic_rs::from_slice(&response.bytes().await?)?)
101    }
102
103    pub(crate) async fn get_query<T, E, R>(&self, url: T, query: E) -> Result<R, Error>
104    where
105        T: AsRef<str>,
106        E: Serialize,
107        R: DeserializeOwned,
108    {
109        let response = self
110            .client()
111            .await?
112            .get(SfacgClient::HOST.to_string() + url.as_ref())
113            .query(&query)
114            .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
115            .header("sfsecurity", self.sf_security()?)
116            .send()
117            .await?;
118
119        Ok(sonic_rs::from_slice(&response.bytes().await?)?)
120    }
121
122    pub(crate) async fn post<T, E, R>(&self, url: T, json: E) -> Result<R, Error>
123    where
124        T: AsRef<str>,
125        E: Serialize,
126        R: DeserializeOwned,
127    {
128        let response = self
129            .client()
130            .await?
131            .post(SfacgClient::HOST.to_string() + url.as_ref())
132            .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
133            .header("sfsecurity", self.sf_security()?)
134            .json(&json)
135            .send()
136            .await?;
137
138        Ok(sonic_rs::from_slice(&response.bytes().await?)?)
139    }
140
141    pub(crate) async fn put<T, E, R>(&self, url: T, json: E) -> Result<R, Error>
142    where
143        T: AsRef<str>,
144        E: Serialize,
145        R: DeserializeOwned,
146    {
147        let response = self
148            .client()
149            .await?
150            .put(SfacgClient::HOST.to_string() + url.as_ref())
151            .basic_auth(SfacgClient::USERNAME, Some(SfacgClient::PASSWORD))
152            .header("sfsecurity", self.sf_security()?)
153            .json(&json)
154            .send()
155            .await?;
156
157        Ok(sonic_rs::from_slice(&response.bytes().await?)?)
158    }
159
160    pub(crate) async fn get_rss(&self, url: &Url) -> Result<Response, Error> {
161        let response = self.client_rss().await?.get(url.clone()).send().await?;
162        crate::check_status(response.status(), format!("HTTP request failed: `{url}`"))?;
163
164        Ok(response)
165    }
166
167    fn sf_security(&self) -> Result<String, Error> {
168        let uuid = Uuid::new_v4();
169        let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
170        let device_token = crate::uid();
171
172        let sign = crate::md5_hex(
173            format!("{uuid}{timestamp}{device_token}{}", SfacgClient::SALT),
174            AsciiCase::Upper,
175        );
176
177        Ok(format!(
178            "nonce={uuid}&timestamp={timestamp}&devicetoken={device_token}&sign={sign}"
179        ))
180    }
181
182    pub(crate) fn convert(content: String) -> String {
183        let mut result = String::with_capacity(content.len());
184
185        for c in content.chars() {
186            result.push(*CHARACTER_MAPPER.get(&c).unwrap_or(&c));
187        }
188
189        result
190    }
191}