tencentcloud_sms_sdk/core/
client.rs

1//! Main client for TencentCloud API requests
2
3use 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
13/// Main client for TencentCloud SMS API
14pub struct Client {
15    /// Credentials for authentication
16    credential: Credential,
17    /// Region for API requests
18    region: String,
19    /// Client configuration profile
20    profile: ClientProfile,
21    /// HTTP client
22    http_client: reqwest::Client,
23    /// Service name (always "sms" for SMS service)
24    service: String,
25    /// TC3 signer for request signing
26    signer: Tc3Signer,
27}
28
29impl Client {
30    /// Create a new client with credentials and region
31    ///
32    /// # Arguments
33    ///
34    /// * `credential` - TencentCloud credentials
35    /// * `region` - Region for API requests (e.g., "ap-guangzhou")
36    ///
37    /// # Examples
38    ///
39    /// ```rust
40    /// use tencentcloud_sms_sdk::{Client, Credential};
41    ///
42    /// let credential = Credential::new("your_secret_id", "your_secret_key", None);
43    /// let client = Client::new(credential, "ap-guangzhou");
44    /// ```
45    pub fn new<S: Into<String>>(credential: Credential, region: S) -> Self {
46        Self::with_profile(credential, region, ClientProfile::new())
47    }
48
49    /// Create a new client with custom profile
50    ///
51    /// # Arguments
52    ///
53    /// * `credential` - TencentCloud credentials
54    /// * `region` - Region for API requests
55    /// * `profile` - Client configuration profile
56    ///
57    /// # Examples
58    ///
59    /// ```rust
60    /// use tencentcloud_sms_sdk::{Client, Credential, ClientProfile, HttpProfile};
61    ///
62    /// let credential = Credential::new("your_secret_id", "your_secret_key", None);
63    /// let mut http_profile = HttpProfile::new();
64    /// http_profile.set_req_timeout(30);
65    /// let client_profile = ClientProfile::with_http_profile(http_profile);
66    /// let client = Client::with_profile(credential, "ap-guangzhou", client_profile);
67    /// ```
68    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        // Configure proxy if set
86        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    /// Send SMS message
114    ///
115    /// # Arguments
116    ///
117    /// * `request` - SendSmsRequest containing SMS parameters
118    ///
119    /// # Examples
120    ///
121    /// ```rust,no_run
122    /// use tencentcloud_sms_sdk::{Client, Credential, SendSmsRequest};
123    ///
124    /// #[tokio::main]
125    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
126    ///     let credential = Credential::new("your_secret_id", "your_secret_key", None);
127    ///     let client = Client::new(credential, "ap-guangzhou");
128    ///     
129    ///     let request = SendSmsRequest::new(
130    ///         vec!["+8613800000000".to_string()],
131    ///         "1400000000",
132    ///         "123456",
133    ///         "YourSignature",
134    ///         vec!["123456".to_string()],
135    ///     );
136    ///     
137    ///     let response = client.send_sms(request).await?;
138    ///     println!("Response: {:?}", response);
139    ///     Ok(())
140    /// }
141    /// ```
142    pub async fn send_sms(&self, request: SendSmsRequest) -> Result<SendSmsResponse> {
143        self.make_request("SendSms", &request).await
144    }
145
146    /// Make an API request
147    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        // Validate credentials
153        self.credential.validate()?;
154
155        // Serialize request body
156        let payload = serde_json::to_string(request)?;
157
158        // Current timestamp
159        let timestamp = Utc::now();
160
161        // Build headers
162        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        // Add session token if available
184        if let Some(token) = self.credential.token() {
185            headers.insert("X-TC-Token".to_string(), token.to_string());
186        }
187
188        // Prepare headers for signing
189        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        // Sign the request using TC3 signer
195        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        // Create authorization header
206        let authorization = self
207            .signer
208            .create_authorization_header(&result, signed_headers);
209        headers.insert("Authorization".to_string(), authorization);
210
211        // Build HTTP request
212        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        // Add headers
220        for (key, value) in headers {
221            request_builder = request_builder.header(&key, &value);
222        }
223
224        // Add body for POST requests
225        if self.profile.get_http_profile().req_method == "POST" {
226            request_builder = request_builder.body(payload.clone());
227        }
228
229        // Send request
230        let response = request_builder.send().await?;
231
232        // Check status code
233        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        // Get response text
242        let response_text = response.text().await?;
243
244        // Debug logging
245        if self.profile.is_debug() {
246            log::debug!("Request: {}", payload);
247            log::debug!("Response: {}", response_text);
248        }
249
250        // Parse response
251        let response_json: serde_json::Value = serde_json::from_str(&response_text)?;
252
253        // Check for API errors
254        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        // Extract the actual response data
277        let response_data = response_json
278            .get("Response")
279            .ok_or_else(|| TencentCloudError::other("Invalid response format"))?;
280
281        // Deserialize response
282        let result: R = serde_json::from_value(response_data.clone())?;
283
284        Ok(result)
285    }
286
287    /// Get the region
288    pub fn region(&self) -> &str {
289        &self.region
290    }
291
292    /// Get the service name
293    pub fn service(&self) -> &str {
294        &self.service
295    }
296
297    /// Get the client profile
298    pub fn profile(&self) -> &ClientProfile {
299        &self.profile
300    }
301
302    /// Set a new region
303    pub fn set_region<S: Into<String>>(&mut self, region: S) {
304        self.region = region.into();
305    }
306
307    /// Update the client profile
308    pub fn set_profile(&mut self, profile: ClientProfile) {
309        self.profile = profile.clone();
310        // Update signer with new debug setting
311        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    /// Update credentials
320    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}