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