novel_api/ciyuanji/
utils.rs

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