1use std::sync::RwLock;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use hex_simd::AsciiCase;
5use http::HeaderMap;
6use reqwest::Response;
7use reqwest::header::HeaderValue;
8use serde::Serialize;
9use serde::de::DeserializeOwned;
10use sonic_rs::JsonValueMutTrait;
11use tokio::sync::OnceCell;
12use url::Url;
13use uuid::Uuid;
14
15use super::Config;
16use crate::{CiyuanjiClient, Error, HTTPClient, NovelDB};
17
18impl CiyuanjiClient {
19 const APP_NAME: &'static str = "ciyuanji";
20 const HOST: &'static str = "https://api.hwnovel.com/api/ciyuanji/client";
21
22 pub(crate) const OK: &'static str = "200";
23 pub(crate) const FAILED: &'static str = "400";
24 pub(crate) const ALREADY_SIGNED_IN: &'static str = "410";
25 pub(crate) const ALREADY_SIGNED_IN_MSG: &'static str = "今日已签到";
26 pub(crate) const ALREADY_ORDERED_MSG: &'static str = "暂无可购买章节";
27
28 const VERSION: &'static str = "3.4.6";
29 const PLATFORM: &'static str = "1";
30 const CHANNEL: &'static str = "100";
31
32 const USER_AGENT: &'static str = "Mozilla/5.0 (Linux; Android 11; Pixel 4 XL Build/RP1A.200720.009; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/92.0.4515.115 Mobile Safari/537.36";
33 const USER_AGENT_RSS: &'static str =
34 "Dalvik/2.1.0 (Linux; U; Android 14; sdk_gphone64_arm64 Build/UE1A.230829.050)";
35
36 pub(crate) const DES_KEY: &'static str = "ZUreQN0E";
37 const KEY_PARAM: &'static str = "NpkTYvpvhJjEog8Y051gQDHmReY54z5t3F0zSd9QEFuxWGqfC8g8Y4GPuabq0KPdxArlji4dSnnHCARHnkqYBLu7iIw55ibTo18";
38
39 pub async fn new() -> Result<Self, Error> {
41 let config: Option<Config> = crate::load_config_file(CiyuanjiClient::APP_NAME)?;
42
43 Ok(Self {
44 proxy: None,
45 no_proxy: false,
46 cert_path: None,
47 client: OnceCell::new(),
48 client_rss: OnceCell::new(),
49 db: OnceCell::new(),
50 config: RwLock::new(config),
51 })
52 }
53
54 #[must_use]
55 pub(crate) fn try_token(&self) -> String {
56 if self.has_token() {
57 self.config
58 .read()
59 .unwrap()
60 .as_ref()
61 .unwrap()
62 .token
63 .to_string()
64 } else {
65 String::default()
66 }
67 }
68
69 #[must_use]
70 pub(crate) fn has_token(&self) -> bool {
71 self.config.read().unwrap().is_some()
72 }
73
74 pub(crate) fn save_token(&self, config: Config) {
75 *self.config.write().unwrap() = Some(config);
76 }
77
78 pub(crate) async fn db(&self) -> Result<&NovelDB, Error> {
79 self.db
80 .get_or_try_init(|| async { NovelDB::new(CiyuanjiClient::APP_NAME).await })
81 .await
82 }
83
84 pub(crate) async fn client(&self) -> Result<&HTTPClient, Error> {
85 self.client
86 .get_or_try_init(|| async {
87 let mut headers = HeaderMap::new();
88 headers.insert("version", HeaderValue::from_static(CiyuanjiClient::VERSION));
89 headers.insert(
90 "platform",
91 HeaderValue::from_static(CiyuanjiClient::PLATFORM),
92 );
93 headers.insert("channel", HeaderValue::from_static(CiyuanjiClient::CHANNEL));
94 headers.insert(
95 "deviceno",
96 HeaderValue::from_str(
97 &crate::uid().as_simple().to_string().to_lowercase()[0..16],
98 )?,
99 );
100
101 HTTPClient::builder()
102 .app_name(CiyuanjiClient::APP_NAME)
103 .user_agent(CiyuanjiClient::USER_AGENT.to_string())
104 .headers(headers)
105 .maybe_proxy(self.proxy.clone())
106 .no_proxy(self.no_proxy)
107 .maybe_cert_path(self.cert_path.clone())
108 .retry_host(CiyuanjiClient::HOST)
109 .build()
110 .await
111 })
112 .await
113 }
114
115 pub(crate) async fn client_rss(&self) -> Result<&HTTPClient, Error> {
116 self.client_rss
117 .get_or_try_init(|| async {
118 HTTPClient::builder()
119 .app_name(CiyuanjiClient::APP_NAME)
120 .user_agent(CiyuanjiClient::USER_AGENT_RSS.to_string())
121 .maybe_proxy(self.proxy.clone())
122 .no_proxy(self.no_proxy)
123 .maybe_cert_path(self.cert_path.clone())
124 .build()
125 .await
126 })
127 .await
128 }
129
130 pub(crate) async fn get<T, R>(&self, url: T) -> Result<R, Error>
131 where
132 T: AsRef<str>,
133 R: DeserializeOwned,
134 {
135 let response = self
136 .client()
137 .await?
138 .get(CiyuanjiClient::HOST.to_string() + url.as_ref())
139 .query(&GenericRequest::new(sonic_rs::json!({}))?)
140 .header("token", self.try_token())
141 .send()
142 .await?;
143 crate::check_status(
144 response.status(),
145 format!("HTTP request failed: `{}`", url.as_ref()),
146 )?;
147
148 Ok(sonic_rs::from_slice(&response.bytes().await?)?)
149 }
150
151 pub(crate) async fn get_query<T, E, R>(&self, url: T, query: E) -> Result<R, Error>
152 where
153 T: AsRef<str>,
154 E: Serialize,
155 R: DeserializeOwned,
156 {
157 let mut count = 0;
158
159 let response = loop {
160 let response = self
161 .client()
162 .await?
163 .get(CiyuanjiClient::HOST.to_string() + url.as_ref())
164 .query(&GenericRequest::new(&query)?)
165 .header("token", self.try_token())
166 .send()
167 .await;
168
169 if let Ok(response) = response {
170 break response;
171 } else {
172 tracing::info!(
173 "HTTP request failed: `{}`, retry, number of times: `{}`",
174 response.as_ref().unwrap_err(),
175 count + 1
176 );
177
178 count += 1;
179 if count > 3 {
180 response?;
181 }
182 }
183 };
184
185 crate::check_status(
186 response.status(),
187 format!("HTTP request failed: `{}`", url.as_ref()),
188 )?;
189
190 Ok(sonic_rs::from_slice(&response.bytes().await?)?)
191 }
192
193 pub(crate) async fn post<T, E, R>(&self, url: T, json: E) -> Result<R, Error>
194 where
195 T: AsRef<str>,
196 E: Serialize,
197 R: DeserializeOwned,
198 {
199 let response = self
200 .client()
201 .await?
202 .post(CiyuanjiClient::HOST.to_string() + url.as_ref())
203 .json(&GenericRequest::new(json)?)
204 .header("token", self.try_token())
205 .send()
206 .await?;
207 crate::check_status(
208 response.status(),
209 format!("HTTP request failed: `{}`", url.as_ref()),
210 )?;
211
212 Ok(sonic_rs::from_slice(&response.bytes().await?)?)
213 }
214
215 pub(crate) async fn get_rss(&self, url: &Url) -> Result<Response, Error> {
216 let response = self.client_rss().await?.get(url.clone()).send().await?;
217 crate::check_status(response.status(), format!("HTTP request failed: `{url}`"))?;
218
219 Ok(response)
220 }
221
222 pub(crate) fn do_shutdown(&self) -> Result<(), Error> {
223 if self.has_token() {
224 crate::save_config_file(
225 CiyuanjiClient::APP_NAME,
226 self.config.write().unwrap().take(),
227 )?;
228 } else {
229 tracing::info!("No data can be saved to the configuration file");
230 }
231
232 Ok(())
233 }
234}
235
236impl Drop for CiyuanjiClient {
237 fn drop(&mut self) {
238 if let Err(err) = self.do_shutdown() {
239 tracing::error!("Fail to save config file: `{err}`");
240 }
241 }
242}
243
244#[must_use]
245#[derive(Serialize)]
246#[serde(rename_all = "camelCase")]
247struct GenericRequest {
248 pub param: String,
249 pub request_id: String,
250 pub sign: String,
251 pub timestamp: u128,
252}
253
254impl GenericRequest {
255 fn new<T>(json: T) -> Result<Self, Error>
256 where
257 T: Serialize,
258 {
259 let mut json = sonic_rs::to_value(&json)?;
260
261 let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
262 json.as_object_mut()
263 .unwrap()
264 .insert("timestamp", sonic_rs::json!(timestamp));
265
266 let param = crate::des_ecb_base64_encrypt(CiyuanjiClient::DES_KEY, json.to_string())?;
267
268 let request_id = Uuid::new_v4().as_simple().to_string();
269
270 let sign = crate::md5_hex(
271 base64_simd::STANDARD.encode_to_string(format!(
272 "param={param}&requestId={request_id}×tamp={timestamp}&key={}",
273 CiyuanjiClient::KEY_PARAM
274 )),
275 AsciiCase::Upper,
276 );
277
278 Ok(Self {
279 param,
280 request_id,
281 sign,
282 timestamp,
283 })
284 }
285}
286
287pub(crate) fn check_response_success(code: String, msg: String, ok: bool) -> Result<(), Error> {
288 if code != CiyuanjiClient::OK || !ok {
289 Err(Error::NovelApi(format!(
290 "{} request failed, code: `{code}`, msg: `{}`, ok: `{ok}`",
291 CiyuanjiClient::APP_NAME,
292 msg.trim()
293 )))
294 } else {
295 Ok(())
296 }
297}
298
299pub(crate) fn check_already_signed_in(code: &str, msg: &str) -> bool {
300 code == CiyuanjiClient::ALREADY_SIGNED_IN && msg == CiyuanjiClient::ALREADY_SIGNED_IN_MSG
301}
302
303pub(crate) fn check_already_ordered(code: &str, msg: &str) -> bool {
304 code == CiyuanjiClient::FAILED && msg == CiyuanjiClient::ALREADY_ORDERED_MSG
305}