novel_api/ciyuanji/
utils.rs

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    /// Create a ciyuanji client
40    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}&timestamp={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}