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