1use std::sync::{OnceLock as SyncOnceCell, RwLock};
2
3use hex_simd::AsciiCase;
4use jiff::Zoned;
5use rand::RngExt;
6use rand::distr::Alphanumeric;
7use reqwest::Response;
8use serde::Serialize;
9use serde::de::DeserializeOwned;
10use sonic_rs::{JsonValueMutTrait, Value};
11use tokio::sync::OnceCell;
12use url::{Url, form_urlencoded};
13
14use super::Config;
15use crate::{CiweimaoClient, Error, HTTPClient, NovelDB};
16
17impl CiweimaoClient {
18 const APP_NAME: &'static str = "ciweimao";
19
20 pub(crate) const OK: &'static str = "100000";
21 pub(crate) const LOGIN_EXPIRED: &'static str = "200100";
22 pub(crate) const NOT_FOUND: &'static str = "320001";
23 pub(crate) const ALREADY_SIGNED_IN: &'static str = "340001";
24 pub(crate) const NEED_TO_UPGRADE_VERSION: &'static str = "310017";
25
26 pub(crate) const APP_VERSION: &'static str = "2.9.365";
27 pub(crate) const DEVICE_TOKEN: &'static str = "ciweimao_";
28
29 const USER_AGENT: &'static str =
30 "Android com.kuangxiangciweimao.novel.c 2.9.365, Xiaomi, 24030PN60G, 34, 14";
31 const USER_AGENT_RSS: &'static str =
32 "Dalvik/2.1.0 (Linux; U; Android 14; 24030PN60G Build/UKQ1.231003.002)";
33
34 const AES_KEY: &'static str = "sD6doAOcW7hm7iaeK6UlcdtAIWlZGlBr";
35 const HMAC_KEY: &'static str = "a90f3731745f1c30ee77cb13fc00005a";
36 const SIGNATURES: &'static str =
37 const_format::concatcp!(CiweimaoClient::HMAC_KEY, "CkMxWNB666");
38 const PUBLIC_KEY: &'static str = "-----BEGIN PUBLIC KEY-----
39MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxX5AMAGSDhTxsIEahC5t
40Jxypy8qyPijOT2rsMhuUDvENtWpl4axsfLRpD1AlghzBSpNgi1idyZ/OtJFvZsjj
41+drdEO7rCzxMBOlZdw79Gwo06QFSD8JL8X4f49YcGl2+LI5d0KBY2wXdh7urEHQC
42xLK/Lxu9e9ADHXzY26tpCJyvF5LITKZPnzYjGt4fhCEhuoPoeVlJdRAMmGeoRZQ/
43DeRTSAQ1iS3HqalTYRcM4AIiLumivk3vpz8RFsTT0SCKX0zgFRwxkC8pya9/Ls7j
44ALth10rUJTac7fv/801DM6ybAW3IqLgFFUucOwyUF2opRB5AHdoUaa5h4Hb6vwRl
45tQIDAQAB
46-----END PUBLIC KEY-----";
47
48 pub async fn new() -> Result<Self, Error> {
50 let config: Option<Config> = crate::load_config_file(CiweimaoClient::APP_NAME)?;
51
52 Ok(Self {
53 proxy: None,
54 no_proxy: false,
55 cert_path: None,
56 client: OnceCell::new(),
57 client_rss: OnceCell::new(),
58 db: OnceCell::new(),
59 config: RwLock::new(config),
60 })
61 }
62
63 #[must_use]
64 pub(crate) fn try_account(&self) -> String {
65 if self.has_token() {
66 self.config
67 .read()
68 .unwrap()
69 .as_ref()
70 .unwrap()
71 .account
72 .to_string()
73 } else {
74 String::default()
75 }
76 }
77
78 #[must_use]
79 pub(crate) fn try_login_token(&self) -> String {
80 if self.has_token() {
81 self.config
82 .read()
83 .unwrap()
84 .as_ref()
85 .unwrap()
86 .login_token
87 .to_string()
88 } else {
89 String::default()
90 }
91 }
92
93 #[must_use]
94 fn reader_id(&self) -> Option<u32> {
95 if self.has_token() {
96 Some(self.config.read().unwrap().as_ref().unwrap().reader_id)
97 } else {
98 None
99 }
100 }
101
102 #[must_use]
103 pub(crate) fn has_token(&self) -> bool {
104 self.config.read().unwrap().is_some()
105 }
106
107 pub(crate) fn save_token(&self, config: Config) {
108 *self.config.write().unwrap() = Some(config);
109 }
110
111 pub(crate) async fn db(&self) -> Result<&NovelDB, Error> {
112 self.db
113 .get_or_try_init(|| async { NovelDB::new(CiweimaoClient::APP_NAME).await })
114 .await
115 }
116
117 pub(crate) async fn client(&self) -> Result<&HTTPClient, Error> {
118 self.client
119 .get_or_try_init(|| async {
120 HTTPClient::builder()
121 .app_name(CiweimaoClient::APP_NAME)
122 .user_agent(CiweimaoClient::USER_AGENT.to_string())
123 .allow_compress(false)
125 .maybe_proxy(self.proxy.clone())
126 .no_proxy(self.no_proxy)
127 .maybe_cert_path(self.cert_path.clone())
128 .retry_url(Url::parse(self.get_host())?)
129 .build()
130 .await
131 })
132 .await
133 }
134
135 async fn client_rss(&self) -> Result<&HTTPClient, Error> {
136 self.client_rss
137 .get_or_try_init(|| async {
138 HTTPClient::builder()
139 .app_name(CiweimaoClient::APP_NAME)
140 .user_agent(CiweimaoClient::USER_AGENT_RSS.to_string())
141 .maybe_proxy(self.proxy.clone())
142 .no_proxy(self.no_proxy)
143 .maybe_cert_path(self.cert_path.clone())
144 .build()
145 .await
146 })
147 .await
148 }
149
150 pub(crate) async fn get_query<T, E, R>(&self, url: T, query: E) -> Result<R, Error>
151 where
152 T: AsRef<str>,
153 E: Serialize,
154 R: DeserializeOwned,
155 {
156 let response = self
157 .client()
158 .await?
159 .get(self.get_host().to_string() + url.as_ref())
160 .query(&query)
161 .send()
162 .await?;
163 crate::check_status(
164 response.status(),
165 format!("HTTP request failed: `{}`", url.as_ref()),
166 )?;
167
168 Ok(sonic_rs::from_slice(&response.bytes().await?)?)
169 }
170
171 pub(crate) async fn post<T, E, R>(&self, url: T, form: E) -> Result<R, Error>
172 where
173 T: AsRef<str>,
174 E: Serialize,
175 R: DeserializeOwned,
176 {
177 let mut count = 0;
178
179 let response = loop {
180 let response = self
181 .client()
182 .await?
183 .post(self.get_host().to_string() + url.as_ref())
184 .header("charsets", "utf-8")
185 .form(&self.append_param(&form)?)
186 .send()
187 .await;
188
189 if let Ok(response) = response {
190 break response;
191 } else {
192 tracing::info!(
193 "HTTP request failed: `{}`, retry, number of times: `{}`",
194 response.as_ref().unwrap_err(),
195 count + 1
196 );
197
198 count += 1;
199 if count > 3 {
200 response?;
201 }
202 }
203 };
204
205 crate::check_status(
206 response.status(),
207 format!("HTTP request failed: `{}`", url.as_ref()),
208 )?;
209
210 let bytes = response.bytes().await?;
211 let bytes = crate::aes_256_cbc_no_iv_base64_decrypt(CiweimaoClient::get_aes_key(), &bytes)?;
212
213 Ok(sonic_rs::from_slice(&bytes)?)
214 }
215
216 pub(crate) async fn get_rss(&self, url: &Url) -> Result<Response, Error> {
217 let response = self.client_rss().await?.get(url.clone()).send().await?;
218 crate::check_status(response.status(), format!("HTTP request failed: `{url}`"))?;
219
220 Ok(response)
221 }
222
223 fn append_param<T>(&self, query: T) -> Result<Value, Error>
224 where
225 T: Serialize,
226 {
227 let mut value = sonic_rs::to_value(&query)?;
228 let object = value.as_object_mut().unwrap();
229
230 object.insert("app_version", CiweimaoClient::APP_VERSION);
231 object.insert("device_token", CiweimaoClient::DEVICE_TOKEN);
232
233 let rand_str = CiweimaoClient::get_rand_str();
234 object.insert("rand_str", &rand_str);
235
236 let p = self.hmac(&rand_str)?;
237 object.insert("p", &p);
238
239 if self.has_token() {
240 object.insert("account", &self.try_account());
241 object.insert("login_token", &self.try_login_token());
242 }
243 Ok(value)
244 }
245
246 #[must_use]
247 fn get_host(&self) -> &'static str {
248 if let Some(reader_id) = self.reader_id() {
249 let last_char = reader_id.to_string().chars().last().unwrap();
250
251 if ('1'..='5').contains(&last_char) {
252 return "https://app1.happybooker.cn";
253 }
254 }
255
256 "https://app1.hbooker.com"
257 }
258
259 #[must_use]
260 fn get_aes_key() -> &'static [u8] {
261 static AES_KEY: SyncOnceCell<Vec<u8>> = SyncOnceCell::new();
262 AES_KEY
263 .get_or_init(|| crate::sha256(CiweimaoClient::AES_KEY.as_bytes()))
264 .as_ref()
265 }
266
267 pub(crate) fn hashvalue(&self, timestamp: u128) -> Result<String, Error> {
268 Ok(crate::md5_hex(
269 crate::aes_256_cbc_no_iv_base64_encrypt(
270 CiweimaoClient::get_aes_key(),
271 format!("{}{timestamp}", self.try_account()),
272 )?,
273 AsciiCase::Lower,
274 ))
275 }
276
277 #[must_use]
278 fn get_rand_str() -> String {
279 let now = Zoned::now();
280
281 let rand_str: String = rand::rng()
282 .sample_iter(&Alphanumeric)
283 .take(12)
284 .map(|c| char::from(c).to_lowercase().to_string())
285 .collect();
286
287 format!("{}{rand_str}", now.strftime("%M%S"))
288 }
289
290 fn hmac(&self, rand_str: &str) -> Result<String, Error> {
291 let msg: String = form_urlencoded::Serializer::new(String::new())
292 .append_pair("account", &self.try_account())
293 .append_pair("app_version", CiweimaoClient::APP_VERSION)
294 .append_pair("rand_str", rand_str)
295 .append_pair("signatures", CiweimaoClient::SIGNATURES)
296 .finish();
297
298 crate::hmac_sha256_base64(CiweimaoClient::HMAC_KEY, msg.as_bytes())
299 }
300
301 pub(crate) fn rsa_encrypt(plaintext: &str) -> Result<String, Error> {
302 crate::rsa_base64_encrypt(CiweimaoClient::PUBLIC_KEY, plaintext)
303 }
304
305 pub(crate) fn do_shutdown(&self) -> Result<(), Error> {
306 if self.has_token() {
307 crate::save_config_file(
308 CiweimaoClient::APP_NAME,
309 self.config.write().unwrap().take(),
310 )?;
311 } else {
312 tracing::info!("No data can be saved to the configuration file");
313 }
314
315 Ok(())
316 }
317}
318
319impl Drop for CiweimaoClient {
320 fn drop(&mut self) {
321 if let Err(err) = self.do_shutdown() {
322 tracing::error!("Fail to save config file: `{err}`");
323 }
324 }
325}
326
327pub(crate) fn check_response_success(code: String, tip: Option<String>) -> Result<(), Error> {
328 if code != CiweimaoClient::OK {
329 Err(Error::NovelApi(format!(
330 "{} request failed, code: `{code}`, msg: `{}`",
331 CiweimaoClient::APP_NAME,
332 tip.unwrap().trim()
333 )))
334 } else {
335 Ok(())
336 }
337}
338
339pub(crate) fn check_already_signed_in(code: &str) -> bool {
340 code == CiweimaoClient::ALREADY_SIGNED_IN
341}