novel_api/sfacg/
utils.rs

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