translation_api_cn/tencent/
mod.rs

1use crate::Limit;
2use hmac::{
3    digest::{CtOutput as HmacOutput, InvalidLength},
4    Hmac, Mac,
5};
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8use std::collections::HashMap;
9use time::OffsetDateTime;
10
11mod region;
12pub use region::Region;
13
14// 预计会被删除的模块
15#[cfg(test)]
16pub mod ser_json;
17
18mod hash;
19pub use hash::*;
20
21mod response;
22pub use response::{Response, ResponseError, ResponseInner};
23
24pub const URL: &str = "https://tmt.tencentcloudapi.com";
25
26/// HMAC-SHA256 算法
27pub type HmacSha256 = Hmac<Sha256>;
28pub type Output = HmacOutput<HmacSha256>;
29pub type Result<T> = std::result::Result<T, Error>;
30
31#[derive(thiserror::Error, Debug)]
32pub enum Error {
33    #[error("序列化时出错")]
34    Ser(#[from] serde_json::Error),
35    #[error("计算 HMAC-SHA256 时出错")]
36    Hash(#[from] InvalidLength),
37    #[error("计算 unix timestamp 时出错")]
38    UnixTimeStamp(#[from] time::error::ComponentRange),
39}
40
41/// 翻译前的必要信息
42///
43/// https://cloud.tencent.com/document/product/551/40566
44#[derive(Debug, Serialize)]
45pub struct Query<'q> {
46    /// 翻译源语言,可设置为 auto
47    ///
48    /// TODO:变成 enum 类型
49    #[serde(rename = "Source")]
50    pub from:      &'q str,
51    /// 翻译目标语言,不可设置为 auto
52    ///
53    /// TODO:和 `from` 共用 enum 类型,但是并非任意两个语言之间可以互译。
54    /// 比如 `ar(阿拉伯语):en(英语)` 表明阿拉伯语只能从英语中翻译过去。
55    /// 请求翻译 query,必须为 UTF-8 编码。
56    ///
57    /// TODO: 在传入之前应该把文字控制在 2000 以内,
58    /// 一个汉字、一个字母、一个标点都计为一个字符,
59    /// 超过 2000 字节要分段请求。
60    #[serde(rename = "Target")]
61    pub to:        &'q str,
62    #[serde(rename = "ProjectId")]
63    pub projectid: u8,
64    #[serde(rename = "SourceTextList")]
65    pub q:         &'q [&'q str],
66}
67
68impl<'q> Query<'q> {
69    #[rustfmt::skip]
70    pub fn new(q: &'q [&'q str], from: &'q str, to: &'q str, projectid: u8) -> Self {
71        Self { q, from, to, projectid }
72    }
73
74    pub fn to_hashed(&self) -> Result<String> { Ok(hash256(&serde_json::to_vec(self)?)) }
75
76    pub fn to_json_string(&self) -> Result<String> {
77        serde_json::to_string(self).map_err(|e| e.into())
78    }
79
80    // 由于 [`reqwest::RequestBuilder::json`] 调用了 [`serde_json::to_vec`],
81    // 直接调用 [`reqwest::RequestBuilder::json`] 会触发
82    // `{"Error":{"Code":"AuthFailure.SignatureFailure","Message":"The provided credentials
83    // could not be validated. Please check your signature is correct."}"}}`
84    // 。
85    #[cfg(test)]
86    pub fn to_hashed2(&self) -> Result<String> { Ok(hash256(&ser_json::to_vec(self)?)) }
87
88    #[cfg(test)]
89    pub fn to_json_string2(&self) -> Result<String> {
90        ser_json::to_string(self).map_err(|e| e.into())
91    }
92}
93
94/// 账户信息以及一些不变的信息
95/// 需要:机器翻译(TMT)全读写访问权限
96#[derive(Debug, Deserialize)]
97#[serde(rename = "tencent")] // for config or cmd
98pub struct User {
99    /// SecretId
100    pub id:        String,
101    /// SecretKey
102    pub key:       String,
103    /// 地域列表,默认为北京。
104    #[serde(default)]
105    pub region:    Region,
106    /// 项目ID,可以根据控制台-账号中心-项目管理中的配置填写,如无配置请填写默认项目ID:0
107    #[serde(default = "default_projectid")]
108    pub projectid: u8,
109    /// 每秒并发请求,默认为 5。
110    #[serde(default = "default_qps")]
111    // #[serde(skip_deserializing)]
112    pub qps: u8,
113    /// 每秒并发请求的限制,默认为 Char(2000)。
114    #[serde(default = "default_limit")]
115    // #[serde(skip_deserializing)]
116    pub limit: Limit,
117}
118
119fn default_qps() -> u8 { 5 }
120fn default_limit() -> Limit { Limit::Char(2000) }
121fn default_projectid() -> u8 { 0 }
122
123impl Default for User {
124    fn default() -> Self {
125        Self { id:        String::new(),
126               key:       String::new(),
127               region:    Region::default(),
128               projectid: 0,
129               qps:       5,
130               limit:     default_limit(), }
131    }
132}
133
134/// 生成请求结构
135#[derive(Debug)]
136pub struct Header<'u, 'q> {
137    pub datetime:         OffsetDateTime,
138    pub timestamp:        String,
139    pub credential_scope: String,
140    pub authorization:    String,
141    pub user:             &'u User,
142    pub query:            &'q Query<'q>,
143}
144
145impl<'u, 'q> Header<'u, 'q> {
146    const ACTION: &'static str = "TextTranslateBatch";
147    const ALGORITHM: &'static str = "TC3-HMAC-SHA256";
148    const CANONICALHEADERS: &'static str =
149        "content-type:application/json\nhost:tmt.tencentcloudapi.com\n";
150    // const CANONICALHEADERS: &'static str =
151    //     "content-type:application/json; charset=utf-8\nhost:cvm.tencentcloudapi.com\n";
152    const CANONICALQUERYSTRING: &'static str = "";
153    const CANONICALURI: &'static str = "/";
154    const CONTENTTYPE: &'static str = "application/json";
155    // const CONTENTTYPE: &'static str = "application/json; charset=utf-8";
156    const CREDENTIALSCOPE: &'static str = "tc3_request";
157    const HOST: &'static str = "tmt.tencentcloudapi.com";
158    const HTTPREQUESTMETHOD: &'static str = "POST";
159    const SERVICE: &'static str = "tmt";
160    const SIGNEDHEADERS: &'static str = "content-type;host";
161    const VERSION: &'static str = "2018-03-21";
162
163    #[rustfmt::skip]
164    pub fn new(user: &'u User, query: &'q Query) -> Self {
165        let datetime = OffsetDateTime::now_utc();
166        let timestamp = datetime.unix_timestamp().to_string();
167        Self { datetime, timestamp, credential_scope: String::new(),
168               authorization: String::new(), user, query }
169    }
170
171    pub fn signature(&mut self) -> Result<String> {
172        let canonical_request = format!("{}\n{}\n{}\n{}\n{}\n{}",
173                                        Self::HTTPREQUESTMETHOD,
174                                        Self::CANONICALURI,
175                                        Self::CANONICALQUERYSTRING,
176                                        Self::CANONICALHEADERS,
177                                        Self::SIGNEDHEADERS,
178                                        self.query.to_hashed()?);
179
180        let date = self.datetime.date();
181        self.credential_scope = format!("{}/{}/{}", date, Self::SERVICE, Self::CREDENTIALSCOPE);
182        let stringtosign = format!("{}\n{}\n{}\n{}",
183                                   Self::ALGORITHM,
184                                   self.timestamp,
185                                   self.credential_scope,
186                                   hash256(canonical_request.as_bytes()));
187        let secret_date =
188            hash_2u8(format!("TC3{}", self.user.key).as_bytes(), format!("{date}").as_bytes())?;
189        let secret_service = hash_hash_u8(secret_date, Self::SERVICE.as_bytes())?;
190        let secret_signing = hash_hash_u8(secret_service, Self::CREDENTIALSCOPE.as_bytes())?;
191        Ok(hmac_sha256_string(hash_hash_u8(secret_signing, stringtosign.as_bytes())?))
192    }
193
194    pub fn authorization(&mut self) -> Result<&str> {
195        let signature = self.signature()?;
196        self.authorization = format!("{} Credential={}/{}, SignedHeaders={}, Signature={}",
197                                     Self::ALGORITHM,
198                                     self.user.id,
199                                     self.credential_scope,
200                                     Self::SIGNEDHEADERS,
201                                     signature);
202        Ok(&self.authorization)
203    }
204
205    pub fn header(&self) -> HashMap<&str, &str> {
206        let mut map = HashMap::with_capacity(8);
207        map.insert("authorization", self.authorization.as_str()).unwrap_or_default();
208        map.insert("content-type", Self::CONTENTTYPE).unwrap_or_default();
209        map.insert("host", Self::HOST).unwrap_or_default();
210        map.insert("x-tc-action", Self::ACTION).unwrap_or_default();
211        map.insert("x-tc-version", Self::VERSION).unwrap_or_default();
212        map.insert("x-tc-region", self.user.region.as_str()).unwrap_or_default();
213        map.insert("x-tc-timestamp", &self.timestamp).unwrap_or_default();
214        map
215    }
216}