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