1use crate::{api::APIResponse, WEATHER_API_URL, WEATHER_DEV_API_URL};
2use log::trace;
3use md5::{Digest, Md5};
4use reqwest::{Client, ClientBuilder};
5use serde_json::Value;
6use std::collections::BTreeMap;
7
8pub struct QWeatherClient {
10 api_host: String,
11 client: Client,
13 base_params: BTreeMap<String, String>,
15 client_config: ClientConfig,
17}
18
19pub struct ClientConfig {
21 pub public_id: String,
23 pub private_key: String,
25 pub subscription: bool,
27 pub lang: Option<String>,
29 pub unit: Option<String>,
31}
32
33impl ClientConfig {
34 pub fn new(public_id: impl ToString, private_key: impl ToString) -> Self {
36 ClientConfig {
37 public_id: public_id.to_string(),
38 private_key: private_key.to_string(),
39 subscription: false,
40 lang: Some("zh".to_string()),
41 unit: None,
42 }
43 }
44}
45
46impl QWeatherClient {
47 pub fn with_config(client_config: ClientConfig) -> Self {
49 let api_host = if client_config.subscription {
50 WEATHER_API_URL.to_string()
51 } else {
52 WEATHER_DEV_API_URL.to_string()
53 };
54
55 let client = ClientBuilder::new()
56 .gzip(true)
57 .build()
58 .expect("Failed to create reqwest client");
59
60 let mut base_params = BTreeMap::new();
61 base_params.insert("publicid".to_string(), client_config.public_id.to_string());
62 if let Some(lang) = &client_config.lang {
63 base_params.insert("lang".to_string(), lang.to_string());
64 }
65
66 QWeatherClient {
67 api_host,
68 client,
69 base_params,
70 client_config,
71 }
72 }
73
74 pub fn new(
76 public_id: impl ToString,
77 private_key: impl ToString,
78 subscription: bool,
79 lang: impl ToString,
80 unit: impl ToString,
81 ) -> Self {
82 let api_host = if subscription {
83 WEATHER_API_URL.to_string()
84 } else {
85 WEATHER_DEV_API_URL.to_string()
86 };
87
88 let client = ClientBuilder::new()
89 .gzip(true)
90 .build()
91 .expect("Failed to create reqwest client");
92
93 let mut base_params = BTreeMap::new();
94 base_params.insert("publicid".to_string(), public_id.to_string());
95
96 QWeatherClient {
97 api_host,
98 client,
99 base_params,
100 client_config: ClientConfig {
101 public_id: public_id.to_string(),
102 private_key: private_key.to_string(),
103 subscription,
104 lang: Some(lang.to_string()),
105 unit: Some(unit.to_string()),
106 },
107 }
108 }
109
110 pub fn get_api_host(&self) -> &str {
112 &self.api_host
113 }
114
115 pub async fn request_api<T>(
117 &self,
118 url: String,
119 mut params: BTreeMap<String, String>,
120 ) -> Result<APIResponse<T>, reqwest::Error>
121 where
122 T: serde::de::DeserializeOwned,
123 {
124 params.extend(self.base_params.clone());
126 params.insert(
127 "t".to_string(),
128 chrono::Local::now().timestamp().to_string(),
129 );
130 let sign = self.sign_params(¶ms);
131 params.insert("sign".to_string(), sign);
132 match self.client.get(&url).query(¶ms).send().await {
133 Ok(response) => {
134 let body: Value = response.json().await?;
135 trace!("Response: {:?}", body);
136 match body["code"].as_str() {
137 Some("200") | None => match serde_json::from_value::<T>(body) {
138 Ok(response) => Ok(APIResponse::Success(response)),
139 Err(e) => {
140 log::error!("Failed to parse response: {}", e);
141 Ok(APIResponse::Error("Failed to parse response".to_string()))
142 }
143 },
144 Some(code) => Ok(APIResponse::Error(code.to_string())),
146 }
147 }
148 Err(error) => Ok(APIResponse::Error(error.to_string())),
149 }
150 }
151
152 fn sign_params(&self, params: &BTreeMap<String, String>) -> String {
154 let mut sign = String::new();
155
156 for (key, value) in params {
157 if key.to_lowercase() == "sign" || key.to_lowercase() == "key" {
158 continue;
159 }
160 if value.is_empty() {
161 continue;
162 }
163 sign.push_str(&format!("{}={}&", key, value));
164 }
165 sign.pop();
166 sign.push_str(&self.client_config.private_key);
167 let mut hasher = Md5::new();
168 hasher.update(&sign);
169 let sign = format!("{:x}", hasher.finalize());
170
171 sign
172 }
173}