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 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}×tamp={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}