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_MSG: &'static str = "今日已签到";
25    pub(crate) const ALREADY_ORDERED_MSG: &'static str = "暂无可购买章节";
26
27    const VERSION: &'static str = "3.4.6";
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 14; sdk_gphone64_arm64 Build/UE1A.230829.050)";
33
34    pub(crate) const DES_KEY: &'static str = "ZUreQN0E";
35    const KEY_PARAM: &'static str = "NpkTYvpvhJjEog8Y051gQDHmReY54z5t3F0zSd9QEFuxWGqfC8g8Y4GPuabq0KPdxArlji4dSnnHCARHnkqYBLu7iIw55ibTo18";
36
37    /// Create a ciyuanji client
38    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                    .retry_host(CiyuanjiClient::HOST)
100                    .build()
101                    .await
102            })
103            .await
104    }
105
106    pub(crate) async fn client_rss(&self) -> Result<&HTTPClient, Error> {
107        self.client_rss
108            .get_or_try_init(|| async {
109                HTTPClient::builder()
110                    .app_name(CiyuanjiClient::APP_NAME)
111                    .user_agent(CiyuanjiClient::USER_AGENT_RSS.to_string())
112                    .maybe_proxy(self.proxy.clone())
113                    .no_proxy(self.no_proxy)
114                    .maybe_cert_path(self.cert_path.clone())
115                    .build()
116                    .await
117            })
118            .await
119    }
120
121    pub(crate) async fn get<T, R>(&self, url: T) -> Result<R, Error>
122    where
123        T: AsRef<str>,
124        R: DeserializeOwned,
125    {
126        let response = self
127            .client()
128            .await?
129            .get(CiyuanjiClient::HOST.to_string() + url.as_ref())
130            .query(&GenericRequest::new(sonic_rs::json!({}))?)
131            .header("token", self.try_token())
132            .send()
133            .await?;
134        crate::check_status(
135            response.status(),
136            format!("HTTP request failed: `{}`", url.as_ref()),
137        )?;
138
139        Ok(sonic_rs::from_slice(&response.bytes().await?)?)
140    }
141
142    pub(crate) async fn get_query<T, E, R>(&self, url: T, query: E) -> Result<R, Error>
143    where
144        T: AsRef<str>,
145        E: Serialize,
146        R: DeserializeOwned,
147    {
148        let response = self
149            .client()
150            .await?
151            .get(CiyuanjiClient::HOST.to_string() + url.as_ref())
152            .query(&GenericRequest::new(&query)?)
153            .header("token", self.try_token())
154            .send()
155            .await?;
156        crate::check_status(
157            response.status(),
158            format!("HTTP request failed: `{}`", url.as_ref()),
159        )?;
160
161        Ok(sonic_rs::from_slice(&response.bytes().await?)?)
162    }
163
164    pub(crate) async fn post<T, E, R>(&self, url: T, json: E) -> Result<R, Error>
165    where
166        T: AsRef<str>,
167        E: Serialize,
168        R: DeserializeOwned,
169    {
170        let response = self
171            .client()
172            .await?
173            .post(CiyuanjiClient::HOST.to_string() + url.as_ref())
174            .json(&GenericRequest::new(json)?)
175            .header("token", self.try_token())
176            .send()
177            .await?;
178        crate::check_status(
179            response.status(),
180            format!("HTTP request failed: `{}`", url.as_ref()),
181        )?;
182
183        Ok(sonic_rs::from_slice(&response.bytes().await?)?)
184    }
185
186    pub(crate) async fn get_rss(&self, url: &Url) -> Result<Response, Error> {
187        let response = self.client_rss().await?.get(url.clone()).send().await?;
188        crate::check_status(response.status(), format!("HTTP request failed: `{url}`"))?;
189
190        Ok(response)
191    }
192
193    pub(crate) fn do_shutdown(&self) -> Result<(), Error> {
194        if self.has_token() {
195            crate::save_config_file(
196                CiyuanjiClient::APP_NAME,
197                self.config.write().unwrap().take(),
198            )?;
199        } else {
200            tracing::info!("No data can be saved to the configuration file");
201        }
202
203        Ok(())
204    }
205}
206
207impl Drop for CiyuanjiClient {
208    fn drop(&mut self) {
209        if let Err(err) = self.do_shutdown() {
210            tracing::error!("Fail to save config file: `{err}`");
211        }
212    }
213}
214
215#[must_use]
216#[derive(Serialize)]
217#[serde(rename_all = "camelCase")]
218struct GenericRequest {
219    pub param: String,
220    pub request_id: String,
221    pub sign: String,
222    pub timestamp: u128,
223}
224
225impl GenericRequest {
226    fn new<T>(json: T) -> Result<Self, Error>
227    where
228        T: Serialize,
229    {
230        let mut json = sonic_rs::to_value(&json)?;
231
232        let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
233        json.as_object_mut()
234            .unwrap()
235            .insert("timestamp", sonic_rs::json!(timestamp));
236
237        let param = crate::des_ecb_base64_encrypt(CiyuanjiClient::DES_KEY, json.to_string())?;
238
239        let request_id = Uuid::new_v4().as_simple().to_string();
240
241        let sign = crate::md5_hex(
242            base64_simd::STANDARD.encode_to_string(format!(
243                "param={param}&requestId={request_id}&timestamp={timestamp}&key={}",
244                CiyuanjiClient::KEY_PARAM
245            )),
246            AsciiCase::Upper,
247        );
248
249        Ok(Self {
250            param,
251            request_id,
252            sign,
253            timestamp,
254        })
255    }
256}
257
258pub(crate) fn check_response_success(code: String, msg: String, ok: bool) -> Result<(), Error> {
259    if code != CiyuanjiClient::OK || !ok {
260        Err(Error::NovelApi(format!(
261            "{} request failed, code: `{code}`, msg: `{}`, ok: `{ok}`",
262            CiyuanjiClient::APP_NAME,
263            msg.trim()
264        )))
265    } else {
266        Ok(())
267    }
268}
269
270pub(crate) fn check_already_signed_in(code: &str, msg: &str) -> bool {
271    code == CiyuanjiClient::FAILED && msg == CiyuanjiClient::ALREADY_SIGNED_IN_MSG
272}
273
274pub(crate) fn check_already_ordered(code: &str, msg: &str) -> bool {
275    code == CiyuanjiClient::FAILED && msg == CiyuanjiClient::ALREADY_ORDERED_MSG
276}