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