wx_sdk/
mp.rs

1//! This module define the api set of wechat office account.
2//!
3//! It seperates those apis to different mods by url path.
4
5use std::collections::HashMap;
6
7use roxmltree::Document;
8
9use crate::{
10    access_token::AccessTokenProvider,
11    error::{CommonError, SdkError},
12    mp::event::signature::Signature,
13};
14use crate::{
15    wechat::{WxApiRequestBuilder, WxSdk},
16    SdkResult,
17};
18
19use self::{
20    customservice::CustomServiceModule, datacube::DataCubeModule, draft::DraftModule,
21    freepublish::FreePublishModule, material::MaterialModule, media::MediaModule, menu::MenuModule,
22    message::MessageModule, qrcode::QrcodeModule, reply::Reply, shorten::ShortenModule,
23    sns::SnsModule, tags::TagsModule, template::TemplateModule, ticket::TicketModule,
24    user::UserModule,
25};
26pub mod customservice;
27pub mod datacube;
28pub mod draft;
29pub mod event;
30pub mod freepublish;
31pub mod material;
32pub mod media;
33pub mod menu;
34pub mod message;
35pub mod qrcode;
36pub mod reply;
37pub mod shorten;
38pub mod sns;
39pub mod tags;
40pub mod template;
41pub mod ticket;
42pub mod user;
43
44/// The configuration of your app server.
45#[derive(Clone)]
46pub struct ServerConfig {
47    pub token: String,
48    pub encoding_mode: EncodingMode,
49}
50
51type AesKey = String;
52
53/// Encoding mode of message getting or sending with wechat.
54/// [EncodingMode::Compat] or [EncodingMode::Security] mode has a aes-key.
55#[derive(Clone)]
56pub enum EncodingMode {
57    Plain,
58    Compat(AesKey),
59    Security(AesKey),
60}
61
62impl ServerConfig {
63    pub fn new<S: AsRef<str>>(token: S, encoding_mode: EncodingMode) -> Self {
64        ServerConfig {
65            token: token.as_ref().to_owned(),
66            encoding_mode,
67        }
68    }
69}
70
71/// 公众号接口SDK,由于 Rust Doc 中还无法搜索中文,请直接搜索相关请求 url 中的关键信息,例如 `clear_quota`为接口限额清零接口。
72#[derive(Clone)]
73pub struct MpSdk<T: AccessTokenProvider> {
74    pub(crate) sdk: WxSdk<T>,
75    pub(crate) server_config: ServerConfig,
76}
77
78impl<T: AccessTokenProvider> MpSdk<T> {
79    /// 接口限额清零
80    ///
81    /// 公众号调用接口并不是无限制的。
82    /// 每个帐号每月共10次清零操作机会,清零生效一次即用掉一次机会(10次包括了平台上的清零和调用接口API的清零)。
83    pub async fn clear_quota(&self) -> SdkResult<()> {
84        let base_url = "https://api.weixin.qq.com/cgi-bin/clear_quota";
85        let sdk = &self.sdk;
86        let app_id = sdk.app_id.clone();
87        let res = sdk
88            .wx_post(base_url)
89            .await?
90            .json(&serde_json::json!({ "appid": app_id }))
91            .send()
92            .await?
93            .json::<CommonError>()
94            .await?;
95
96        res.into()
97    }
98
99    /// Qrcode generator module 生成二维码模块
100    pub fn qrcode(&self) -> QrcodeModule<WxSdk<T>> {
101        QrcodeModule(&self.sdk)
102    }
103    /// Short key generator module 短key生成模块
104    pub fn shorten(&self) -> ShortenModule<WxSdk<T>> {
105        ShortenModule(&self.sdk)
106    }
107    /// Tag module 标签模块
108    pub fn tags(&self) -> TagsModule<WxSdk<T>> {
109        TagsModule(&self.sdk)
110    }
111    /// User module 用户模块
112    pub fn user(&self) -> UserModule<WxSdk<T>> {
113        UserModule(&self.sdk)
114    }
115    /// Message send module 消息(发送)相关模块
116    pub fn message(&self) -> MessageModule<WxSdk<T>> {
117        MessageModule(&self.sdk)
118    }
119    /// Menu module 自定义菜单模块
120    pub fn menu(&self) -> MenuModule<WxSdk<T>> {
121        MenuModule(&self.sdk)
122    }
123    /// Template message module 模板消息模块
124    pub fn template(&self) -> TemplateModule<WxSdk<T>> {
125        TemplateModule(&self.sdk)
126    }
127
128    /// Media module (临时)素材文件模块
129    pub fn media(&self) -> MediaModule<WxSdk<T>> {
130        MediaModule(&self.sdk)
131    }
132
133    /// Material module (永久)素材模块
134    pub fn material(&self) -> MaterialModule<WxSdk<T>> {
135        MaterialModule(&self.sdk)
136    }
137
138    /// Datacube module 分析中心模块
139    pub fn datacube(&self) -> DataCubeModule<WxSdk<T>> {
140        DataCubeModule(&self.sdk)
141    }
142
143    /// Custom Service module 客服模块
144    pub fn customservice(&self) -> CustomServiceModule<WxSdk<T>> {
145        CustomServiceModule(&self.sdk)
146    }
147
148    /// 获取jsapi ticket 或者 wx_card ticket
149    pub fn ticket(&self) -> TicketModule<T> {
150        TicketModule(&self.sdk)
151    }
152
153    /// 网页授权模块
154    pub fn sns(&self) -> SnsModule<T> {
155        SnsModule(&self.sdk)
156    }
157
158    /// 草稿箱模块
159    pub fn draft(&self) -> DraftModule<T> {
160        DraftModule(&self.sdk)
161    }
162
163    /// 草稿箱模块
164    pub fn freepublish(&self) -> FreePublishModule<T> {
165        FreePublishModule(&self.sdk)
166    }
167
168    /// 解析微信推送消息
169    pub fn parse_received_msg<S: AsRef<str>>(
170        &self,
171        msg: S,
172        url_params: Option<HashMap<String, String>>,
173    ) -> SdkResult<event::ReceivedEvent> {
174        let server_config = &self.server_config;
175        let msg = match server_config.encoding_mode {
176            EncodingMode::Plain => event::ReceivedEvent::parse(msg.as_ref()),
177            EncodingMode::Compat(_) => event::ReceivedEvent::parse(msg.as_ref()),
178            EncodingMode::Security(ref aes_key) => {
179                let url_params = url_params
180                    .ok_or_else(|| SdkError::InvalidParams("needs url_params".to_owned()))?;
181                let signature = url_params
182                    .get("msg_signature")
183                    .ok_or_else(|| SdkError::InvalidParams("msg_signature".to_owned()))?;
184                let timestamp = url_params
185                    .get("timestamp")
186                    .ok_or_else(|| SdkError::InvalidParams("timestamp".to_owned()))?;
187                let nonce = url_params
188                    .get("nonce")
189                    .ok_or_else(|| SdkError::InvalidParams("nonce".to_owned()))?;
190                let token = server_config.token.clone();
191                let root = Document::parse(msg.as_ref())?;
192                let encrypt_msg = root
193                    .descendants()
194                    .find(|n| n.has_tag_name("Encrypt"))
195                    .map(|n| n.text())
196                    .flatten()
197                    .unwrap();
198
199                let check_sign = vec![
200                    token,
201                    timestamp.clone(),
202                    nonce.clone(),
203                    encrypt_msg.to_owned(),
204                ];
205                let sign = event::signature::Signature::new(signature, check_sign);
206                if !sign.is_ok() {
207                    return Err(SdkError::InvalidSignature);
208                }
209                // decrpyted_text = [random(16) + content_len(4) + content + appid]
210                let (msg, app_id) = event::crypto::decrypt_message(encrypt_msg, aes_key)?;
211                if app_id != self.sdk.app_id {
212                    return Err(SdkError::InvalidAppid);
213                }
214                event::ReceivedEvent::parse(msg.as_ref())
215            }
216        };
217        msg
218    }
219
220    /// 得到回复消息 XML
221    pub fn reply_to_xml<S: Into<String>>(
222        &self,
223        reply: Reply,
224        from: S,
225        to: S,
226        url_params: Option<HashMap<String, String>>,
227    ) -> SdkResult<String> {
228        let server_config = &self.server_config;
229        let mut reply_xml = reply::reply_to_xml(reply, from, to)?;
230        if let EncodingMode::Security(ref aes_key) = server_config.encoding_mode {
231            let ref app_id = self.sdk.app_id;
232            let encrypt_msg = event::crypto::encrypt_message(&reply_xml, aes_key, app_id)?;
233            let url_params =
234                url_params.ok_or_else(|| SdkError::InvalidParams("needs url_params".to_owned()))?;
235
236            let timestamp = url_params
237                .get("timestamp")
238                .ok_or_else(|| SdkError::InvalidParams("timestamp".to_owned()))?;
239            let nonce = url_params
240                .get("nonce")
241                .ok_or_else(|| SdkError::InvalidParams("nonce".to_owned()))?;
242            let token = server_config.token.clone();
243            let check_sign = vec![
244                token,
245                timestamp.clone(),
246                nonce.clone(),
247                encrypt_msg.to_owned(),
248            ];
249            let msg_signaturet = Signature::generate_signature(check_sign);
250            reply_xml = format!(
251                "<xml>
252<Encrypt><![CDATA[{}]]></Encrypt>
253<MsgSignature><![CDATA[{}]]></MsgSignature>
254<TimeStamp>{}</TimeStamp>
255<Nonce><![CDATA[{}]]></Nonce>
256</xml>",
257                encrypt_msg, msg_signaturet, timestamp, nonce
258            );
259        }
260        Ok(reply_xml)
261    }
262}