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