tencentcloud_sms_sdk/core/
client.rs1use crate::core::{ClientProfile, Credential};
4use crate::error::{Result, TencentCloudError};
5use crate::sms::{SendSmsRequest, SendSmsResponse};
6use chrono::Utc;
7use reqwest;
8use serde_json;
9use std::collections::HashMap;
10use std::time::Duration;
11use tencentcloud_sign_sdk::{sha256_hex, Tc3Signer};
12
13pub struct Client {
15 credential: Credential,
17 region: String,
19 profile: ClientProfile,
21 http_client: reqwest::Client,
23 service: String,
25 signer: Tc3Signer,
27}
28
29impl Client {
30 pub fn new<S: Into<String>>(credential: Credential, region: S) -> Self {
46 Self::with_profile(credential, region, ClientProfile::new())
47 }
48
49 pub fn with_profile<S: Into<String>>(
69 credential: Credential,
70 region: S,
71 profile: ClientProfile,
72 ) -> Self {
73 let http_profile = profile.get_http_profile();
74
75 let mut client_builder = reqwest::Client::builder()
76 .timeout(http_profile.get_req_timeout())
77 .connect_timeout(http_profile.get_connect_timeout())
78 .tcp_keepalive(if http_profile.keep_alive {
79 Some(Duration::from_secs(60))
80 } else {
81 None
82 })
83 .user_agent(&http_profile.user_agent);
84
85 if let Some(proxy_url) = http_profile.get_proxy_url() {
87 if let Ok(proxy) = reqwest::Proxy::all(&proxy_url) {
88 client_builder = client_builder.proxy(proxy);
89 }
90 }
91
92 let http_client = client_builder
93 .build()
94 .unwrap_or_else(|_| reqwest::Client::new());
95
96 let signer = Tc3Signer::new(
97 credential.secret_id().to_string(),
98 credential.secret_key().to_string(),
99 "sms".to_string(),
100 profile.is_debug(),
101 );
102
103 Self {
104 credential,
105 region: region.into(),
106 profile,
107 http_client,
108 service: "sms".to_string(),
109 signer,
110 }
111 }
112
113 pub async fn send_sms(&self, request: SendSmsRequest) -> Result<SendSmsResponse> {
143 self.make_request("SendSms", &request).await
144 }
145
146 async fn make_request<T, R>(&self, action: &str, request: &T) -> Result<R>
148 where
149 T: serde::Serialize,
150 R: serde::de::DeserializeOwned,
151 {
152 self.credential.validate()?;
154
155 let payload = serde_json::to_string(request)?;
157
158 let timestamp = Utc::now();
160
161 let mut headers = HashMap::new();
163 headers.insert("Content-Type".to_string(), "application/json".to_string());
164 headers.insert(
165 "Host".to_string(),
166 self.profile.get_http_profile().endpoint.clone(),
167 );
168 headers.insert("X-TC-Action".to_string(), action.to_string());
169 headers.insert(
170 "X-TC-Version".to_string(),
171 self.profile.get_api_version().to_string(),
172 );
173 headers.insert("X-TC-Region".to_string(), self.region.clone());
174 headers.insert(
175 "X-TC-Timestamp".to_string(),
176 timestamp.timestamp().to_string(),
177 );
178 headers.insert(
179 "X-TC-Language".to_string(),
180 self.profile.get_language().to_string(),
181 );
182
183 if let Some(token) = self.credential.token() {
185 headers.insert("X-TC-Token".to_string(), token.to_string());
186 }
187
188 let host = self.profile.get_http_profile().endpoint.clone();
190 let canonical_headers = format!("content-type:application/json\nhost:{}\n", host);
191 let signed_headers = "content-type;host";
192 let hashed_payload = sha256_hex(&payload);
193
194 let result = self.signer.sign(
196 &self.profile.get_http_profile().req_method,
197 "/",
198 "",
199 &canonical_headers,
200 signed_headers,
201 &hashed_payload,
202 timestamp.timestamp(),
203 );
204
205 let authorization = self
207 .signer
208 .create_authorization_header(&result, signed_headers);
209 headers.insert("Authorization".to_string(), authorization);
210
211 let url = self.profile.get_http_profile().get_full_endpoint();
213 let mut request_builder = match self.profile.get_http_profile().req_method.as_str() {
214 "GET" => self.http_client.get(&url),
215 "POST" => self.http_client.post(&url),
216 _ => self.http_client.post(&url),
217 };
218
219 for (key, value) in headers {
221 request_builder = request_builder.header(&key, &value);
222 }
223
224 if self.profile.get_http_profile().req_method == "POST" {
226 request_builder = request_builder.body(payload.clone());
227 }
228
229 let response = request_builder.send().await?;
231
232 if !response.status().is_success() {
234 return Err(TencentCloudError::other(format!(
235 "HTTP error: {} - {}",
236 response.status(),
237 response.text().await.unwrap_or_default()
238 )));
239 }
240
241 let response_text = response.text().await?;
243
244 if self.profile.is_debug() {
246 log::debug!("Request: {}", payload);
247 log::debug!("Response: {}", response_text);
248 }
249
250 let response_json: serde_json::Value = serde_json::from_str(&response_text)?;
252
253 if let Some(error) = response_json.get("Response").and_then(|r| r.get("Error")) {
255 let code = error
256 .get("Code")
257 .and_then(|c| c.as_str())
258 .unwrap_or("Unknown");
259 let message = error
260 .get("Message")
261 .and_then(|m| m.as_str())
262 .unwrap_or("Unknown error");
263 let request_id = response_json
264 .get("Response")
265 .and_then(|r| r.get("RequestId"))
266 .and_then(|r| r.as_str())
267 .map(|s| s.to_string());
268
269 return Err(TencentCloudError::api_with_request_id(
270 code,
271 message,
272 request_id.as_deref(),
273 ));
274 }
275
276 let response_data = response_json
278 .get("Response")
279 .ok_or_else(|| TencentCloudError::other("Invalid response format"))?;
280
281 let result: R = serde_json::from_value(response_data.clone())?;
283
284 Ok(result)
285 }
286
287 pub fn region(&self) -> &str {
289 &self.region
290 }
291
292 pub fn service(&self) -> &str {
294 &self.service
295 }
296
297 pub fn profile(&self) -> &ClientProfile {
299 &self.profile
300 }
301
302 pub fn set_region<S: Into<String>>(&mut self, region: S) {
304 self.region = region.into();
305 }
306
307 pub fn set_profile(&mut self, profile: ClientProfile) {
309 self.profile = profile.clone();
310 self.signer = Tc3Signer::new(
312 self.credential.secret_id().to_string(),
313 self.credential.secret_key().to_string(),
314 "sms".to_string(),
315 profile.is_debug(),
316 );
317 }
318
319 pub fn set_credential(&mut self, credential: Credential) {
321 self.credential = credential.clone();
322 self.signer = Tc3Signer::new(
323 credential.secret_id().to_string(),
324 credential.secret_key().to_string(),
325 "sms".to_string(),
326 self.profile.is_debug(),
327 );
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::core::HttpProfile;
335 use crate::sms::SendSmsRequest;
336
337 #[test]
338 fn test_client_creation() {
339 let credential = Credential::new("test_id", "test_key", None);
340 let client = Client::new(credential, "ap-guangzhou");
341
342 assert_eq!(client.region(), "ap-guangzhou");
343 assert_eq!(client.service(), "sms");
344 }
345
346 #[test]
347 fn test_client_with_profile() {
348 let credential = Credential::new("test_id", "test_key", None);
349 let mut http_profile = HttpProfile::new();
350 http_profile.set_req_timeout(30);
351 let client_profile = ClientProfile::with_http_profile(http_profile);
352 let client = Client::with_profile(credential, "ap-guangzhou", client_profile);
353
354 assert_eq!(client.region(), "ap-guangzhou");
355 assert_eq!(client.profile().get_http_profile().req_timeout, 30);
356 }
357
358 #[test]
359 fn test_client_setters() {
360 let credential = Credential::new("test_id", "test_key", None);
361 let mut client = Client::new(credential, "ap-guangzhou");
362
363 client.set_region("ap-beijing");
364 assert_eq!(client.region(), "ap-beijing");
365
366 let new_credential = Credential::new("new_id", "new_key", None);
367 client.set_credential(new_credential);
368 assert_eq!(client.credential.secret_id(), "new_id");
369 }
370
371 #[tokio::test]
372 async fn test_send_sms_invalid_credentials() {
373 let credential = Credential::new("", "", None);
374 let client = Client::new(credential, "ap-guangzhou");
375
376 let request = SendSmsRequest::new(
377 vec!["+8613800000000".to_string()],
378 "1400000000",
379 "123456",
380 "Test",
381 vec!["123456".to_string()],
382 );
383
384 let result = client.send_sms(request).await;
385 assert!(result.is_err());
386 }
387}