From 265ee73b275c1f4a5322aecfceb6c4bf6342102b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=8D=E6=99=A8?= Date: Wed, 14 Sep 2022 11:32:46 +0800 Subject: [PATCH 1/9] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E7=82=B9=EF=BC=9A=E4=BF=AE=E5=A4=8D=E4=BC=81?= =?UTF-8?q?=E5=BE=AE/=E5=85=AC=E4=BC=97=E5=8F=B7=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/wechat/cryptos/mod.rs | 3 +++ src/wechat/miniapp/mod.rs | 4 ++-- src/wechat/mp/mod.rs | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/wechat/cryptos/mod.rs b/src/wechat/cryptos/mod.rs index 41adfcf..e062129 100644 --- a/src/wechat/cryptos/mod.rs +++ b/src/wechat/cryptos/mod.rs @@ -88,6 +88,9 @@ impl WechatCrypto { timestamp.to_string(), nonce.to_string(), ]; + if !encrypted.is_empty() { + data.push(encrypted.to_string()); + } data.sort(); let data_str = data.join(""); // create a Sha1 object diff --git a/src/wechat/miniapp/mod.rs b/src/wechat/miniapp/mod.rs index e0a33b5..21d0df8 100644 --- a/src/wechat/miniapp/mod.rs +++ b/src/wechat/miniapp/mod.rs @@ -94,9 +94,9 @@ impl WechatMaClient { /// 验证消息的确来自微信服务器. /// 详情(http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN) /// - pub fn check_signature(&self, signature: &str, timestamp: i64, nonce: &str, echo_str: &str) -> LabradorResult { + pub fn check_signature(&self, signature: &str, timestamp: i64, nonce: &str) -> LabradorResult { let crp = WechatCrypto::new(&self.aes_key.to_owned().unwrap_or_default()); - let _ = crp.check_signature(signature, timestamp, nonce, echo_str, &self.token.to_owned().unwrap_or_default())?; + let _ = crp.check_signature(signature, timestamp, nonce, "", &self.token.to_owned().unwrap_or_default())?; Ok(true) } diff --git a/src/wechat/mp/mod.rs b/src/wechat/mp/mod.rs index 1ede874..d06d264 100644 --- a/src/wechat/mp/mod.rs +++ b/src/wechat/mp/mod.rs @@ -253,9 +253,9 @@ impl WechatMpClient { /// 验证消息的确来自微信服务器. /// 详情(http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN) /// - pub fn check_signature(&self, signature: &str, timestamp: i64, nonce: &str, echo_str: &str) -> LabradorResult { + pub fn check_signature(&self, signature: &str, timestamp: i64, nonce: &str) -> LabradorResult { let crp = WechatCrypto::new(&self.aes_key.to_owned().unwrap_or_default()); - let _ = crp.check_signature(signature, timestamp, nonce, echo_str, &self.token.to_owned().unwrap_or_default())?; + let _ = crp.check_signature(signature, timestamp, nonce, "", &self.token.to_owned().unwrap_or_default())?; Ok(true) } From 85e23e4b0921a7652c62774d4379ec3ec86851ad Mon Sep 17 00:00:00 2001 From: ynp <15104511733@163.com> Date: Wed, 14 Sep 2022 11:38:49 +0800 Subject: [PATCH 2/9] add GetOrder CancelOrder --- src/wechat/cp/method.rs | 6 ++++-- src/wechat/cp/tp/license.rs | 31 ++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/wechat/cp/method.rs b/src/wechat/cp/method.rs index 46968d3..6275e5d 100644 --- a/src/wechat/cp/method.rs +++ b/src/wechat/cp/method.rs @@ -164,7 +164,8 @@ pub enum CpLicenseMethod { SubmitOrderJob, ListOrder, GetOrder, - ListOrderCount, + ListOrderAccount, + CancelOrder, ActiveAccount, BatchActiveAccount, GetActiveInfoByCode, @@ -183,7 +184,8 @@ impl CpLicenseMethod { CpLicenseMethod::SubmitOrderJob => String::from("/cgi-bin/license/submit_order_job"), CpLicenseMethod::ListOrder => String::from("/cgi-bin/license/list_order"), CpLicenseMethod::GetOrder => String::from("/cgi-bin/license/get_order"), - CpLicenseMethod::ListOrderCount => String::from("/cgi-bin/license/list_order_account"), + CpLicenseMethod::ListOrderAccount => String::from("/cgi-bin/license/list_order_account"), + CpLicenseMethod::CancelOrder => String::from("/cgi-bin/license/cancel_order"), CpLicenseMethod::ActiveAccount => String::from("/cgi-bin/license/active_account"), CpLicenseMethod::BatchActiveAccount => String::from("/cgi-bin/license/batch_active_account"), CpLicenseMethod::GetActiveInfoByCode => String::from("/cgi-bin/license/get_active_info_by_code"), diff --git a/src/wechat/cp/tp/license.rs b/src/wechat/cp/tp/license.rs index 7e83d78..9d3ae52 100644 --- a/src/wechat/cp/tp/license.rs +++ b/src/wechat/cp/tp/license.rs @@ -84,6 +84,21 @@ impl<'a, T: SessionStore> WechatCpTpLicense<'a, T> { } + ///
+    ///  获取订单列表
+    /// 服务商查询自己某段时间内的平台能力服务订单列表
+    /// 文档地址:文档
+    /// 
+ pub async fn get_order(&self, order_id: &str) -> LabradorResult { + let mut req = json!({ + "order_id": order_id, + }); + let access_token = self.client.get_wechat_provider_token().await?; + let v = self.client.post(WechatCpMethod::License(CpLicenseMethod::GetOrder), vec![(PROVIDER_ACCESS_TOKEN.to_string(), access_token)], req, RequestType::Json).await?.json::()?; + WechatCommonResponse::parse::(v) + } + + ///
     ///  查询指定订单下的平台能力服务帐号列表。
     /// 若为购买帐号的订单或者存量企业的版本付费迁移订单,则返回帐号激活码列表;
@@ -98,11 +113,25 @@ impl<'a, T: SessionStore> WechatCpTpLicense<'a, T> {
             "limit": limit,
         });
         let access_token = self.client.get_wechat_provider_token().await?;
-        let v = self.client.post(WechatCpMethod::License(CpLicenseMethod::ListOrderCount), vec![(PROVIDER_ACCESS_TOKEN.to_string(), access_token)], req, RequestType::Json).await?.json::()?;
+        let v = self.client.post(WechatCpMethod::License(CpLicenseMethod::ListOrderAccount), vec![(PROVIDER_ACCESS_TOKEN.to_string(), access_token)], req, RequestType::Json).await?.json::()?;
         WechatCommonResponse::parse::(v)
     }
 
 
+    /// 
+    ///  取消订单
+    /// 取消接口许可购买和续费订单,只可取消未支付且未失效的订单。
+    /// 文档地址:文档
+    /// 
+ pub async fn cancel_order(&self, corp_id: &str, order_id: &str) -> LabradorResult { + let mut req = json!({ + "corpid": corp_id, + "order_id": order_id, + }); + let access_token = self.client.get_wechat_provider_token().await?; + self.client.post(WechatCpMethod::License(CpLicenseMethod::CancelOrder), vec![(PROVIDER_ACCESS_TOKEN.to_string(), access_token)], req, RequestType::Json).await?.json::() + } + ///
     ///  激活帐号
     /// 下单购买帐号并支付完成之后,先调用获取订单中的帐号列表接口获取到帐号激活码,

From 71553be17bf58a7d198da2045a016a766a8465d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=BE=8D=E6=99=A8?= 
Date: Wed, 14 Sep 2022 13:00:05 +0800
Subject: [PATCH 3/9] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BC=BA=E9=99=B7?=
 =?UTF-8?q?=EF=BC=9A=E4=BF=AE=E5=A4=8D=E8=A7=A3=E6=9E=90=E4=BC=81=E5=BE=AE?=
 =?UTF-8?q?=E5=9B=9E=E8=B0=83=E6=B6=88=E6=81=AF=E6=96=B9=E6=B3=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/util/prp.rs           | 36 +++++++++++++++++++++++++++++++-----
 src/wechat/cp/tp/mod.rs   | 10 ++++++++++
 src/wechat/cryptos/mod.rs | 32 ++++++++++++++++++++++++--------
 3 files changed, 65 insertions(+), 13 deletions(-)

diff --git a/src/util/prp.rs b/src/util/prp.rs
index 8485cf2..bfda20a 100644
--- a/src/util/prp.rs
+++ b/src/util/prp.rs
@@ -46,31 +46,57 @@ impl PrpCrypto {
     }
 
     /// # 加密消息(aes_128_cbc)
-    pub fn aes_128_cbc_encrypt_msg(&self, plaintext: &str, _id: &str) -> LabradorResult {
+    pub fn aes_128_cbc_encrypt_msg(&self, plaintext: &str, id: Option<&str>) -> LabradorResult {
         let mut wtr = PrpCrypto::get_random_string().into_bytes();
         wtr.write_u32::((plaintext.len() as u32).to_be()).unwrap_or_default();
         wtr.extend(plaintext.bytes());
-        wtr.extend(_id.bytes());
+        if let Some(id) = id {
+            wtr.extend(id.bytes());
+        }
         let encrypted = symm::encrypt(symm::Cipher::aes_128_cbc(), &self.key, Some(&self.key[..16]), &wtr)?;
         let b64encoded = base64::encode(&encrypted);
         Ok(b64encoded)
     }
 
     /// # 解密消息(aes_128_cbc)
-    pub fn aes_128_cbc_decrypt_msg(&self, ciphertext: &str, _id: &str) -> LabradorResult {
+    pub fn aes_128_cbc_decrypt_msg(&self, ciphertext: &str, id: Option<&str>) -> LabradorResult {
         let b64decoded = base64::decode(ciphertext)?;
         let text = symm::decrypt(symm::Cipher::aes_128_cbc(), &self.key, Some(&self.key[..16]), &b64decoded)?;
         let mut rdr = Cursor::new(text[16..20].to_vec());
         let content_length = u32::from_be(rdr.read_u32::().unwrap_or_default()) as usize;
         let content = &text[20 .. content_length + 20];
         let from_id = &text[content_length + 20 ..];
-        if from_id != _id.as_bytes() {
-            return Err(LabraError::InvalidAppId);
+        if let Some(id) = id {
+            if from_id != id.as_bytes() {
+                return Err(LabraError::InvalidAppId);
+            }
         }
         let content_string = String::from_utf8(content.to_vec()).unwrap_or_default();
         Ok(content_string)
     }
 
+    /// # 加密消息(aes_256_cbc)
+    pub fn aes_256_cbc_encrypt_msg(&self, plaintext: &str) -> LabradorResult {
+        let mut wtr = PrpCrypto::get_random_string().into_bytes();
+        wtr.write_u32::((plaintext.len() as u32).to_be()).unwrap_or_default();
+        wtr.extend(plaintext.bytes());
+        let encrypted = symm::encrypt(symm::Cipher::aes_256_cbc(), &self.key, Some(&self.key[..16]), &wtr)?;
+        let b64encoded = base64::encode(&encrypted);
+        Ok(b64encoded)
+    }
+
+    /// # 解密消息(aes_256_cbc)
+    pub fn aes_256_cbc_decrypt_msg(&self, ciphertext: &str) -> LabradorResult {
+        let b64decoded = base64::decode(ciphertext)?;
+        let text = symm::decrypt(symm::Cipher::aes_256_cbc(), &self.key, Some(&self.key[..16]), &b64decoded)?;
+        let mut rdr = Cursor::new(text[16..20].to_vec());
+        let content_length = u32::from_be(rdr.read_u32::().unwrap_or_default()) as usize;
+        let content = &text[20 .. content_length + 20];
+        let from_id = &text[content_length + 20 ..];
+        let content_string = String::from_utf8(content.to_vec()).unwrap_or_default();
+        Ok(content_string)
+    }
+
 
     /// # 解密数据(aes_128_cbc)
     pub fn aes_128_cbc_decrypt_data(&self, ciphertext: &str, iv: &str) -> LabradorResult {
diff --git a/src/wechat/cp/tp/mod.rs b/src/wechat/cp/tp/mod.rs
index 71e91ba..61e8e99 100644
--- a/src/wechat/cp/tp/mod.rs
+++ b/src/wechat/cp/tp/mod.rs
@@ -121,6 +121,16 @@ impl WechatCpTpClient {
         Ok(true)
     }
 
+    /// 
+    /// 检验消息的真实性,并且获取解密后的明文.
+    /// 详情请见: 文档
+    /// 
+ pub fn decrypt_content(&self, signature: &str, timestamp: i64, nonce: &str, data: &str) -> LabradorResult { + let crp = WechatCrypto::new(&self.aes_key.to_owned().unwrap_or_default()); + let res = crp.decrypt_content(data, signature, timestamp, nonce,&self.token.to_owned().unwrap_or_default())?; + Ok(res) + } + /// 获得suite_ticket,不强制刷新suite_ticket /// 由微信服务器推送 pub fn get_suite_ticket(&self) -> LabradorResult { diff --git a/src/wechat/cryptos/mod.rs b/src/wechat/cryptos/mod.rs index e062129..019a22d 100644 --- a/src/wechat/cryptos/mod.rs +++ b/src/wechat/cryptos/mod.rs @@ -77,7 +77,7 @@ impl WechatCrypto { } } - /// #获取签名 + /// # 获取签名 /// /// timestamp 时间戳 /// nonce 随机字符串 @@ -119,7 +119,7 @@ impl WechatCrypto { PrpCrypto::hmac_sha256_sign(key, message) } - /// #数据解密 + /// # 数据解密 /// /// session_key key /// iv 偏移量 @@ -131,7 +131,7 @@ impl WechatCrypto { Ok(msg) } - /// #检查签名 + /// # 检查签名 /// /// timestamp 时间戳 /// nonce 随机字符串 @@ -146,14 +146,14 @@ impl WechatCrypto { Ok(true) } - /// #加密消息 + /// # 加密消息 /// /// timestamp 时间戳 /// nonce 随机字符串 /// msg 加密数据 pub fn encrypt_message(&self, msg: &str, timestamp: i64, nonce: &str, token: &str, id: &str) -> LabradorResult { let prp = PrpCrypto::new(self.key.to_owned()); - let encrypted_msg = prp.aes_128_cbc_encrypt_msg(msg, id)?; + let encrypted_msg = prp.aes_128_cbc_encrypt_msg(msg, id.into())?; let signature = self.get_signature(timestamp, nonce, &encrypted_msg, token); let msg = format!( "\n\ @@ -170,7 +170,7 @@ impl WechatCrypto { Ok(msg) } - /// #解密消息 + /// # 解密消息 /// /// xml 解密内容 /// nonce 随机字符串 @@ -186,11 +186,27 @@ impl WechatCrypto { return Err(LabraError::InvalidSignature("unmatched signature.".to_string())); } let prp = PrpCrypto::new(self.key.to_owned()); - let msg = prp.aes_128_cbc_decrypt_msg(&encrypted_msg, id)?; + let msg = prp.aes_128_cbc_decrypt_msg(&encrypted_msg, id.into())?; Ok(msg) } - /// #解密退款消息 + /// # 检验消息的真实性,并且获取解密后的明文. + ///
    + ///
  1. 利用收到的密文生成安全签名,进行签名验证
  2. + ///
  3. 若验证通过,则提取xml中的加密消息
  4. + ///
  5. 对消息进行解密
  6. + ///
+ pub fn decrypt_content(&self, encrypted_content: &str, signature: &str, timestamp: i64, nonce: &str, token: &str) -> LabradorResult { + let real_signature = self.get_signature(timestamp, nonce, &encrypted_content, token); + if signature != &real_signature { + return Err(LabraError::InvalidSignature("unmatched signature.".to_string())); + } + let prp = PrpCrypto::new(self.key.to_owned()); + let msg = prp.aes_256_cbc_decrypt_msg(&encrypted_content).unwrap(); + Ok(msg) + } + + /// # 解密退款消息 /// /// app_key 应用key /// ciphertext 加密数据 From d6a8d1dd697f1e23a0890d7639ac587dd7e380cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=8D=E6=99=A8?= Date: Thu, 15 Sep 2022 17:08:58 +0800 Subject: [PATCH 4/9] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=201.=E5=A2=9E=E5=8A=A0=E4=BC=81=E5=BE=AE?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=9B=9E=E8=B0=83=E5=A4=84=E7=90=86=202.?= =?UTF-8?q?=E8=B0=83=E6=95=B4xml=E5=8A=A0=E8=A7=A3=E5=AF=86=E6=96=B9?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 1 + src/errors.rs | 8 + src/util/xmlutil.rs | 1 - src/wechat/cp/events/app_admin.rs | 18 + src/wechat/cp/events/approval.rs | 188 ++++++++ src/wechat/cp/events/auth.rs | 43 ++ src/wechat/cp/events/batch_job_result.rs | 56 +++ src/wechat/cp/events/contact_change.rs | 402 ++++++++++++++++++ src/wechat/cp/events/enter_agent.rs | 42 ++ src/wechat/cp/events/location.rs | 54 +++ src/wechat/cp/events/menu.rs | 259 +++++++++++ src/wechat/cp/events/mod.rs | 40 ++ src/wechat/cp/events/permanent_code.rs | 15 + src/wechat/cp/events/share_agent.rs | 21 + src/wechat/cp/events/share_chain.rs | 21 + src/wechat/cp/events/subscribe.rs | 45 ++ src/wechat/cp/events/template_card.rs | 85 ++++ src/wechat/cp/events/ticket.rs | 14 + src/wechat/cp/events/tp_contact_change.rs | 313 ++++++++++++++ src/wechat/cp/events/unsubscribe.rs | 40 ++ src/wechat/cp/messages/image.rs | 46 ++ src/wechat/cp/messages/link.rs | 51 +++ src/wechat/cp/messages/location.rs | 54 +++ src/wechat/cp/messages/mod.rs | 156 +++++++ src/wechat/cp/messages/text.rs | 43 ++ src/wechat/cp/messages/unknown.rs | 16 + src/wechat/cp/messages/video.rs | 47 ++ src/wechat/cp/messages/voice.rs | 48 +++ src/wechat/cp/mod.rs | 12 +- src/wechat/cp/msg_parser.rs | 78 ++++ src/wechat/cp/replies/articles.rs | 168 ++++++++ src/wechat/cp/replies/image.rs | 58 +++ src/wechat/cp/replies/mod.rs | 48 +++ src/wechat/cp/replies/template_card.rs | 266 ++++++++++++ src/wechat/cp/replies/text.rs | 57 +++ src/wechat/cp/replies/update_button.rs | 63 +++ src/wechat/cp/replies/video.rs | 66 +++ src/wechat/cp/replies/voice.rs | 60 +++ src/wechat/cp/tp/license.rs | 4 + src/wechat/cp/tp/mod.rs | 16 +- src/wechat/cryptos/mod.rs | 46 +- src/wechat/miniapp/mod.rs | 4 +- src/wechat/mod.rs | 15 +- src/wechat/mp/events/click.rs | 49 +-- src/wechat/mp/events/location.rs | 55 +-- src/wechat/mp/events/mod.rs | 18 +- .../mp/events/qualification_verify_success.rs | 49 +-- src/wechat/mp/events/scan.rs | 56 +-- src/wechat/mp/events/subscribe.rs | 48 +-- src/wechat/mp/events/subscribe_scan.rs | 52 +-- .../mp/events/template_send_job_finish.rs | 47 +- src/wechat/mp/events/unsubscribe.rs | 50 +-- src/wechat/mp/events/view.rs | 48 +-- src/wechat/mp/messages/image.rs | 48 +-- src/wechat/mp/messages/link.rs | 51 +-- src/wechat/mp/messages/location.rs | 57 +-- src/wechat/mp/messages/mod.rs | 95 ++--- src/wechat/mp/messages/shortvideo.rs | 49 +-- src/wechat/mp/messages/text.rs | 45 +- src/wechat/mp/messages/unknown.rs | 37 +- src/wechat/mp/messages/video.rs | 47 +- src/wechat/mp/messages/voice.rs | 50 +-- src/wechat/mp/mod.rs | 6 +- src/wechat/mp/msg_parser.rs | 48 +++ src/wechat/msg_parser.rs | 46 -- src/wechat/pay/api/wxpay.rs | 6 +- 66 files changed, 3357 insertions(+), 788 deletions(-) create mode 100644 src/wechat/cp/events/app_admin.rs create mode 100644 src/wechat/cp/events/approval.rs create mode 100644 src/wechat/cp/events/auth.rs create mode 100644 src/wechat/cp/events/batch_job_result.rs create mode 100644 src/wechat/cp/events/contact_change.rs create mode 100644 src/wechat/cp/events/enter_agent.rs create mode 100644 src/wechat/cp/events/location.rs create mode 100644 src/wechat/cp/events/menu.rs create mode 100644 src/wechat/cp/events/mod.rs create mode 100644 src/wechat/cp/events/permanent_code.rs create mode 100644 src/wechat/cp/events/share_agent.rs create mode 100644 src/wechat/cp/events/share_chain.rs create mode 100644 src/wechat/cp/events/subscribe.rs create mode 100644 src/wechat/cp/events/template_card.rs create mode 100644 src/wechat/cp/events/ticket.rs create mode 100644 src/wechat/cp/events/tp_contact_change.rs create mode 100644 src/wechat/cp/events/unsubscribe.rs create mode 100644 src/wechat/cp/messages/image.rs create mode 100644 src/wechat/cp/messages/link.rs create mode 100644 src/wechat/cp/messages/location.rs create mode 100644 src/wechat/cp/messages/mod.rs create mode 100644 src/wechat/cp/messages/text.rs create mode 100644 src/wechat/cp/messages/unknown.rs create mode 100644 src/wechat/cp/messages/video.rs create mode 100644 src/wechat/cp/messages/voice.rs create mode 100644 src/wechat/cp/msg_parser.rs create mode 100644 src/wechat/cp/replies/articles.rs create mode 100644 src/wechat/cp/replies/image.rs create mode 100644 src/wechat/cp/replies/mod.rs create mode 100644 src/wechat/cp/replies/template_card.rs create mode 100644 src/wechat/cp/replies/text.rs create mode 100644 src/wechat/cp/replies/update_button.rs create mode 100644 src/wechat/cp/replies/video.rs create mode 100644 src/wechat/cp/replies/voice.rs create mode 100644 src/wechat/mp/msg_parser.rs delete mode 100644 src/wechat/msg_parser.rs diff --git a/Cargo.toml b/Cargo.toml index 0e5f54b..73cef88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ uuid = { version = "0.7.4", features = ["serde", "v4"] } byteorder = {version = "1.3.4"} sxd-document = {version = "0.2", optional= true} sxd-xpath = {version = "0.2", optional= true} +serde-xml-rs = "0.6.0" rustc-serialize = "^0.3" serde_urlencoded = "0.7.1" urlencoding = "2.1.0" diff --git a/src/errors.rs b/src/errors.rs index f534a09..1cced41 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -76,6 +76,14 @@ impl From for LabraError { } } + +impl From for LabraError { + fn from(_err: serde_xml_rs::Error) -> Self { + error!("error to parse xml:{:?}", _err); + LabraError::RedundantField(_err.to_string()) + } +} + impl From for LabraError { fn from(err: ErrorStack) -> Self { LabraError::InvalidSignature(format!("加解密出错:{}", err.to_string())) diff --git a/src/util/xmlutil.rs b/src/util/xmlutil.rs index 79ceaba..1c28bb1 100644 --- a/src/util/xmlutil.rs +++ b/src/util/xmlutil.rs @@ -43,7 +43,6 @@ impl<'d> XPathEvaluator<'d> { &self.variables, &self.namespaces, ); - let v = self.factory.build(xpath).unwrap_or(None).map(|xpath| xpath.evaluate(&context).ok().unwrap_or(Value::String("".to_string()))); v.unwrap_or(Value::String("".to_string())) } diff --git a/src/wechat/cp/events/app_admin.rs b/src/wechat/cp/events/app_admin.rs new file mode 100644 index 0000000..9c916a3 --- /dev/null +++ b/src/wechat/cp/events/app_admin.rs @@ -0,0 +1,18 @@ +use serde::{Serialize, Deserialize}; + +/// 应用管理员变更通知 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpAppAdminChangeEvent { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="AgentID")] + pub agent_id: i64, +} diff --git a/src/wechat/cp/events/approval.rs b/src/wechat/cp/events/approval.rs new file mode 100644 index 0000000..5603e63 --- /dev/null +++ b/src/wechat/cp/events/approval.rs @@ -0,0 +1,188 @@ +use serde::{Serialize, Deserialize}; + +/// 审批状态通知事件 +/// 本事件触发时机为: +/// 1.自建/第三方应用调用审批流程引擎发起申请之后,审批状态发生变化时 +/// 2.自建/第三方应用调用审批流程引擎发起申请之后,在“审批中”状态,有任意审批人进行审批操作时 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpOpenApprovalChangeEvent { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="AgentID")] + pub agent_id: i64, + #[serde(rename="ApprovalInfo")] + pub approval_info: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ApprovalInfo { + /// 审批单编号,由开发者在发起申请时自定义 + #[serde(rename="ThirdNo")] + pub third_no: String, + /// 审批模板名称 + #[serde(rename="OpenSpName")] + pub open_sp_name: String, + /// 审批模板id + #[serde(rename="OpenTemplateId")] + pub open_template_id: i64, + /// 申请单当前审批状态:1-审批中;2-已通过;3-已驳回;4-已取消 + #[serde(rename="OpenSpStatus")] + pub open_sp_status: i32, + #[serde(rename="ApplyTime")] + /// 提交申请时间 + pub apply_time: i64, + /// 提交者姓名 + #[serde(rename="ApplyUserName")] + pub apply_user_name: String, + /// 提交者userid + #[serde(rename="ApplyUserId")] + pub apply_user_id: String, + /// 提交者所在部门 + #[serde(rename="ApplyUserParty")] + pub apply_user_party: String, + /// 提交者头像 + #[serde(rename="ApplyUserImage")] + pub apply_user_image: String, + /// 审批流程信息,可以有多个审批节点 + #[serde(rename="ApprovalNodes")] + pub approval_nodes: ApprovalNodes, + /// 抄送信息,可能有多个抄送人 + #[serde(rename="NotifyNodes")] + pub notify_nodes: NotifyNodes, +} + + +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct ApprovalNodes { + #[serde(rename = "ApprovalNode")] + items: Vec +} + +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct ApprovalNode { + #[serde(rename = "NodeStatus")] + name: u32, + #[serde(rename = "NodeAttr")] + node_attr: u32, + #[serde(rename = "NodeType")] + node_type: u32, + #[serde(rename="Items")] + pub items: Option, +} + +/// 抄送信息,可能有多个抄送人 +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct NotifyNodes { + #[serde(rename = "NotifyNode")] + items: Vec, + /// 当前审批节点:0-第一个审批节点;1-第二个审批节点…以此类推 + #[serde(rename = "approverstep")] + approver_step: Vec +} + +/// 抄送人信息 +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct NotifyNode { + /// 抄送人姓名 + #[serde(rename = "ItemName")] + name: String, + /// 抄送人userid + #[serde(rename = "ItemUserId")] + item_user_id: u32, + /// 抄送人头像 + #[serde(rename = "ItemImage")] + item_image: String, +} + + +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct ApprovalNodeItems { + #[serde(rename = "Item")] + items: Vec +} + +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct ApprovalNodeItem { + /// 分支审批人姓名 + #[serde(rename = "ItemName")] + name: String, + /// 分支审批人userid + #[serde(rename = "ItemUserId")] + item_user_id: u32, + /// 分支审批人头像 + #[serde(rename = "ItemImage")] + item_image: String, + /// 分支审批审批操作状态:1-审批中;2-已同意;3-已驳回;4-已转审 + #[serde(rename = "ItemStatus")] + item_status: u32, + /// 分支审批人审批意见 + #[serde(rename = "ItemSpeech")] + item_speech: u32, + /// 分支审批人操作时间 + #[serde(rename = "ItemOpTime")] + item_op_time: u32, +} + +#[cfg(test)] +mod tests { + use crate::XmlMessageParser; + use super::CpSubscribeEvent; + + #[test] + fn test_from_xml() { + let xml = " + + + 1527838022 + + + 1 + + + + + 1 + 1527837645 + + + + + + + 1 + 1 + 1 + + + + + + 1 + + 0 + + + + + + + + + + + + 0 + +"; + let msg = serde_xml_rs::from_str::(xml).unwrap(); + println!("{}", msg.to_string()); + } +} \ No newline at end of file diff --git a/src/wechat/cp/events/auth.rs b/src/wechat/cp/events/auth.rs new file mode 100644 index 0000000..e3dcc71 --- /dev/null +++ b/src/wechat/cp/events/auth.rs @@ -0,0 +1,43 @@ + +use serde::{Serialize, Deserialize}; + +/// 授权成功通知 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpAuthCreateEvent { + #[serde(rename="SuiteId")] + pub suite_id: String, + #[serde(rename="InfoType")] + pub info_type: String, + #[serde(rename="TimeStamp")] + pub time: i64, + #[serde(rename="AuthCode")] + pub auth_code: String, + #[serde(rename="State")] + pub state: Option, +} +/// 变更授权通知 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpAuthChangeEvent { + #[serde(rename="SuiteId")] + pub suite_id: String, + #[serde(rename="InfoType")] + pub info_type: String, + #[serde(rename="TimeStamp")] + pub time: i64, + #[serde(rename="AuthCorpId")] + pub auth_corp_id: String, + #[serde(rename="State")] + pub state: Option, +} +/// 取消授权通知 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpAuthCancelEvent { + #[serde(rename="SuiteId")] + pub suite_id: String, + #[serde(rename="InfoType")] + pub info_type: String, + #[serde(rename="TimeStamp")] + pub time: i64, + #[serde(rename="AuthCorpId")] + pub auth_corp_id: String, +} \ No newline at end of file diff --git a/src/wechat/cp/events/batch_job_result.rs b/src/wechat/cp/events/batch_job_result.rs new file mode 100644 index 0000000..058fcce --- /dev/null +++ b/src/wechat/cp/events/batch_job_result.rs @@ -0,0 +1,56 @@ +use serde::{Serialize, Deserialize}; + +/// 异步任务完成事件推送 +/// 本事件是成员在使用异步任务接口时,用于接收任务执行完毕的结果通知。 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpBatchJobResultEvent { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="AgentID")] + pub agent_id: i64, + #[serde(rename="BatchJob")] + pub batch_job: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BatchJob { + #[serde(rename="JobId")] + pub job_id: String, + #[serde(rename="JobType")] + pub job_type: String, + #[serde(rename="ErrMsg")] + pub err_msg: String, + #[serde(rename="ErrCode")] + pub err_code: i32, +} + +#[cfg(test)] +mod tests { + use crate::XmlMessageParser; + use super::CpBatchJobResultEvent; + + #[test] + fn test_from_xml() { + let xml = " + + + 123456789 + + + + "; + let msg = CpBatchJobResultEvent::from_xml(xml).unwrap(); + + assert_eq!("fromUser", &msg.source); + assert_eq!("toUser", &msg.target); + assert_eq!("click", &msg.event); + } +} \ No newline at end of file diff --git a/src/wechat/cp/events/contact_change.rs b/src/wechat/cp/events/contact_change.rs new file mode 100644 index 0000000..9f02c72 --- /dev/null +++ b/src/wechat/cp/events/contact_change.rs @@ -0,0 +1,402 @@ +use serde::{Deserialize, Serialize}; + + +// 通讯录变更事件 +// 当企业通过通讯录助手开通通讯录权限后,成员的变更会通知给企业。变更的事件,将推送到企业微信管理端通讯录助手中的‘接收事件服务器’。 +// 由通讯录同步助手调用接口触发的变更事件不回调通讯录同步助手本身。管理员在管理端更改组织架构或者成员信息以及企业微信的成员在客户端变更自己的个人信息将推送给通讯录同步助手 + +/// 新增成员事件 +/// 该事件会回调给通讯录同步助手,代开发自建应用以及上游企业共享的应用 +/// +/// 【重要】对于2022年8月15号后通讯录助手新配置或修改的回调url,成员属性只回调UserId/Department两个字段 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpContactCreateUserEvent { + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="FromUserName")] + pub source: String, + /// 成员名称;代开发自建应用需要管理员授权才返回 + #[serde(rename="Name")] + pub name: Option, + /// 座机;代开发自建应用需要管理员授权才返回。上游共享的应用不返回该字段 + #[serde(rename="Telephone")] + pub telephone: Option, + /// 地址。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Address")] + pub address: Option, + /// 成员别名。上游共享的应用不返回该字段 + #[serde(rename="Alias")] + pub alias: Option, + #[serde(rename="CreateTime")] + pub create_time: i64, + /// 成员UserID + #[serde(rename="UserID")] + pub user_id: String, + /// 职位信息。长度为0~64个字节;代开发自建应用需要管理员授权才返回。上游共享的应用不返回该字段 + #[serde(rename="Position")] + pub position: Option, + /// 头像url。 注:如果要获取小图将url最后的”/0”改成”/100”即可。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Avatar")] + pub avatar: Option, + /// 主部门 + #[serde(rename="MainDepartment")] + pub main_department: i64, + /// 激活状态:1=已激活 2=已禁用 4=未激活 已激活代表已激活企业微信或已关注微信插件(原企业号)5=成员退出 + #[serde(rename="Status")] + pub status: i64, + /// 性别。0表示未定义,1表示男性,2表示女性。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段。注:不可获取指返回值0 + #[serde(rename="Gender")] + pub gender: Option, + /// 成员部门列表,仅返回该应用有查看权限的部门id + #[serde(rename="Department")] + pub department: String, + /// 表示所在部门是否为部门负责人,0-否,1-是,顺序与Department字段的部门逐一对应。上游共享的应用不返回该字段 + #[serde(rename="IsLeaderInDept")] + pub is_leader_in_dept: Option, + /// 直属上级UserID,最多5个。代开发的自建应用和上游共享的应用不返回该字段 + #[serde(rename="DirectLeader")] + pub direct_leader: Option>, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="ChangeType")] + pub change_type: String, + /// 手机号码,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Mobile")] + pub mobile: Option, + /// 邮箱,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Email")] + pub email: String, + /// 企业邮箱,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="BizMail")] + pub biz_mail: Option, + #[serde(rename="ExtAttr")] + pub ext_attrs: Option, +} + + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpExtAttrItemText { + #[serde(rename = "Value")] + pub value: String, +} +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpExtAttrItemWeb { + #[serde(rename="Title")] + pub title: String, + #[serde(rename="Url")] + pub url: String, +} + + + + + +/// 更新成员事件 +/// 该事件会回调给通讯录同步助手,代开发自建应用以及上游企业共享的应用 +/// +/// 【重要】对于2022年8月15号后通讯录助手新配置或修改的回调url,该事件只会在成员所属部门变更或UserId变更的情况下触发,并且成员属性只回调UserId/Department/NewUserId三个字段 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpContactUpdateUserEvent { + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="ChangeType")] + pub change_type: String, + #[serde(rename="MsgType")] + pub msg_type: String, + /// 成员UserID + #[serde(rename="UserID")] + pub user_id: String, + /// 新的UserID,变更时推送(userid由系统生成时可更改一次) + #[serde(rename="NewUserID")] + pub new_user_id: String, + /// 成员名称;代开发自建应用需要管理员授权才返回 + #[serde(rename="Name")] + pub name: Option, + /// 成员部门列表,仅返回该应用有查看权限的部门id + #[serde(rename="Department")] + pub department: String, + /// 主部门 + #[serde(rename="MainDepartment")] + pub main_department: i64, + /// 表示所在部门是否为部门负责人,0-否,1-是,顺序与Department字段的部门逐一对应。上游共享的应用不返回该字段 + #[serde(rename="IsLeaderInDept")] + pub is_leader_in_dept: Option, + /// 职位信息。长度为0~64个字节;代开发自建应用需要管理员授权才返回。上游共享的应用不返回该字段 + #[serde(rename="Position")] + pub position: Option, + /// 手机号码,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Mobile")] + pub mobile: Option, + /// 性别。0表示未定义,1表示男性,2表示女性。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段。注:不可获取指返回值0 + #[serde(rename="Gender")] + pub gender: Option, + /// 邮箱,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Email")] + pub email: String, + /// 激活状态:1=已激活 2=已禁用 4=未激活 已激活代表已激活企业微信或已关注微信插件(原企业号)5=成员退出 + #[serde(rename="Status")] + pub status: i64, + /// 头像url。 注:如果要获取小图将url最后的”/0”改成”/100”即可。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Avatar")] + pub avatar: Option, + /// 成员别名。上游共享的应用不返回该字段 + #[serde(rename="Alias")] + pub alias: Option, + /// 座机;代开发自建应用需要管理员授权才返回。上游共享的应用不返回该字段 + #[serde(rename="Telephone")] + pub telephone: Option, + /// 地址。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Address")] + pub address: Option, + /// 直属上级UserID,最多5个。代开发的自建应用和上游共享的应用不返回该字段 + #[serde(rename="DirectLeader")] + pub direct_leader: Option, + /// 企业邮箱,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="BizMail")] + pub biz_mail: Option, + #[serde(rename="ExtAttr")] + pub ext_attrs: Option, +} + +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct ExtAttrs { + #[serde(rename = "Item")] + items: Vec +} + +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct ExtAttrItem { + #[serde(rename = "Name")] + name: String, + #[serde(rename = "Type")] + attr_type: u32, + #[serde(rename="Text")] + pub text: Option, + #[serde(rename="Web")] + pub web: Option, +} + + + +/// 删除成员事件 +/// 该事件会回调给通讯录同步助手,代开发自建应用以及上游企业共享的应用。 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpContactDeleteUserEvent { + #[serde(rename = "ToUserName")] + pub target: String, + #[serde(rename = "FromUserName")] + pub source: String, + # [serde(rename = "CreateTime")] + pub create_time: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="ChangeType")] + pub change_type: String, + #[serde(rename="MsgType")] + pub msg_type: String, + /// 成员UserID + #[serde(rename="UserID")] + pub user_id: String, +} + + + + +/// 新增部门事件 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpContactCreatePartyEvent { + #[serde(rename = "ToUserName")] + pub target: String, + #[serde(rename = "FromUserName")] + pub source: String, + # [serde(rename = "CreateTime")] + pub create_time: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="ChangeType")] + pub change_type: String, + #[serde(rename="MsgType")] + pub msg_type: String, + /// 部门Id + #[serde(rename="Id")] + pub id: i32, + /// 父部门id + #[serde(rename="ParentId")] + pub parent_id: i32, + /// 部门名称 + #[serde(rename="Name")] + pub name: String, + /// 部门排序 + #[serde(rename="Order")] + pub order: i32, +} + + +/// 更新部门事件 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpContactUpdatePartyEvent { + #[serde(rename = "ToUserName")] + pub target: String, + #[serde(rename = "FromUserName")] + pub source: String, + # [serde(rename = "CreateTime")] + pub create_time: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="ChangeType")] + pub change_type: String, + #[serde(rename="MsgType")] + pub msg_type: String, + /// 部门Id + #[serde(rename="Id")] + pub id: i32, + #[serde(rename="ParentId")] + pub parent_id: i32, + /// 部门名称 + #[serde(rename="Name")] + pub name: String, +} + + + +/// 删除部门事件 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpContactDeletePartyEvent { + #[serde(rename = "ToUserName")] + pub target: String, + #[serde(rename = "FromUserName")] + pub source: String, + # [serde(rename = "CreateTime")] + pub create_time: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="ChangeType")] + pub change_type: String, + #[serde(rename="MsgType")] + pub msg_type: String, + /// 部门Id + #[serde(rename="Id")] + pub id: i32, +} + +/// 标签成员变更事件 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpContactUpdateTagEvent { + #[serde(rename = "ToUserName")] + pub target: String, + #[serde(rename = "FromUserName")] + pub source: String, + # [serde(rename = "CreateTime")] + pub create_time: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="ChangeType")] + pub change_type: String, + #[serde(rename="MsgType")] + pub msg_type: String, + /// 标签Id + #[serde(rename="TagId")] + pub tag_id: i32, + /// 标签中新增的成员userid列表,用逗号分隔 + #[serde(rename="AddUserItems")] + pub add_users: Option, + /// 标签中删除的成员userid列表,用逗号分隔 + #[serde(rename="DelUserItems")] + pub delete_users: Option, + /// 标签中新增的部门id列表,用逗号分隔 + #[serde(rename="AddPartyItems")] + pub add_party: Option, + /// 标签中删除的部门id列表,用逗号分隔 + #[serde(rename="DelPartyItems")] + pub delete_party: Option, +} + + +#[cfg(test)] +mod tests { + use crate::{CpContactUpdateUserEvent, XmlMessageParser}; + use super::CpContactCreateUserEvent; + + #[test] + fn test_create_user_from_xml() { + let xml = " + + + 1403610513 + + + create_user + + + + 1 + + + + 13800000000 + 1 + + + 1 + + + +
+ +
"; + let item: CpContactCreateUserEvent = CpContactCreateUserEvent::from_xml(xml).unwrap(); + println!("{}", serde_json::to_string(&item).unwrap_or_default()); + } + + #[test] + fn test_update_user_from_xml() { + let xml = " + + + 1403610513 + + + update_user + + + + + 1 + + + 13800000000 + 1 + + 1 + + + +
+ + + + 0 + + + + + + + 1 + + <![CDATA[企业微信]]> + + + + +
"; + let item: CpContactUpdateUserEvent = CpContactUpdateUserEvent::from_xml(xml).unwrap(); + println!("{}", serde_json::to_string(&item).unwrap_or_default()); + } +} \ No newline at end of file diff --git a/src/wechat/cp/events/enter_agent.rs b/src/wechat/cp/events/enter_agent.rs new file mode 100644 index 0000000..f32c5fb --- /dev/null +++ b/src/wechat/cp/events/enter_agent.rs @@ -0,0 +1,42 @@ +use serde::{Serialize, Deserialize}; + +/// 进入应用 +/// 本事件在成员进入企业微信的应用时触发 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpEnterAgentEvent { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="AgentID")] + pub agent_id: i64, +} + +#[cfg(test)] +mod tests { + use crate::XmlMessageParser; + use super::CpEnterAgentEvent; + + #[test] + fn test_from_xml() { + let xml = " + + + 123456789 + + + + "; + let msg = CpEnterAgentEvent::from_xml(xml).unwrap(); + + assert_eq!("fromUser", &msg.source); + assert_eq!("toUser", &msg.target); + assert_eq!("click", &msg.event); + } +} \ No newline at end of file diff --git a/src/wechat/cp/events/location.rs b/src/wechat/cp/events/location.rs new file mode 100644 index 0000000..52562a3 --- /dev/null +++ b/src/wechat/cp/events/location.rs @@ -0,0 +1,54 @@ +use serde::{Serialize, Deserialize}; + +/// 上报地理位置 +/// 成员同意上报地理位置后,每次在进入应用会话时都会上报一次地理位置。 +/// 企业可以在管理端修改应用是否需要获取地理位置权限。 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpLocationEvent { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="Latitude")] + pub latitude: f64, + #[serde(rename="Longitude")] + pub longitude: f64, + #[serde(rename="Precision")] + pub precision: f64, + #[serde(rename="AgentID")] + pub agent_id: i64, +} + +#[cfg(test)] +mod tests { + use crate::XmlMessageParser; + use super::CpLocationEvent; + + #[test] + fn test_from_xml() { + let xml = "\ + \ + \ + 123456789\ + \ + \ + 23.137466\ + 113.352425\ + 119.385040\ + "; + let msg = CpLocationEvent::from_xml(xml).unwrap(); + + assert_eq!("fromUser", &msg.source); + assert_eq!("toUser", &msg.target); + assert_eq!("location", &msg.event); + assert_eq!(23, msg.latitude as usize); + assert_eq!(113, msg.longitude as usize); + assert_eq!(119, msg.precision as usize); + } +} \ No newline at end of file diff --git a/src/wechat/cp/events/menu.rs b/src/wechat/cp/events/menu.rs new file mode 100644 index 0000000..539c936 --- /dev/null +++ b/src/wechat/cp/events/menu.rs @@ -0,0 +1,259 @@ +use serde::{Deserialize, Serialize}; + + +// 菜单事件 +// 成员点击自定义菜单后,企业微信会把点击事件推送给应用。 +// 点击菜单弹出子菜单,不会产生上报。 +// 企业微信iPhone1.2.2/Android1.2.2版本开始支持菜单事件,旧版本企业微信成员点击后将没有回应,应用不能正常接收到事件推送。 +// 自定义菜单可以在管理后台的应用设置界面配置。 + +/// 点击菜单拉取消息的事件推送 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpMenuClickEvent { + #[serde(rename = "ToUserName")] + pub target: String, + #[serde(rename = "FromUserName")] + pub source: String, + # [serde(rename = "CreateTime")] + pub create_time: i64, + /// 事件类型:click + #[serde(rename="Event")] + pub event: String, + #[serde(rename="MsgType")] + pub msg_type: String, + /// 事件KEY值,与自定义菜单接口中KEY值对应 + #[serde(rename="EventKey")] + pub event_key: String, + /// 企业应用的id,整型。可在应用的设置页面查看 + #[serde(rename="AgentID")] + pub agent_id: i64, +} + +/// 点击菜单跳转链接的事件推送 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpMenuViewEvent { + #[serde(rename = "ToUserName")] + pub target: String, + #[serde(rename = "FromUserName")] + pub source: String, + # [serde(rename = "CreateTime")] + pub create_time: i64, + /// 事件类型:click + #[serde(rename="Event")] + pub event: String, + #[serde(rename="MsgType")] + pub msg_type: String, + /// 事件KEY值,与自定义菜单接口中KEY值对应 + #[serde(rename="EventKey")] + pub event_key: String, + /// 企业应用的id,整型。可在应用的设置页面查看 + #[serde(rename="AgentID")] + pub agent_id: i64, +} + +/// 扫码推事件的事件推送 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpMenuScanCodePushEvent { + #[serde(rename = "ToUserName")] + pub target: String, + #[serde(rename = "FromUserName")] + pub source: String, + # [serde(rename = "CreateTime")] + pub create_time: i64, + /// 事件类型:click + #[serde(rename="Event")] + pub event: String, + #[serde(rename="MsgType")] + pub msg_type: String, + /// 事件KEY值,与自定义菜单接口中KEY值对应 + #[serde(rename="EventKey")] + pub event_key: String, + /// 企业应用的id,整型。可在应用的设置页面查看 + #[serde(rename="AgentID")] + pub agent_id: i64, + /// 扫描信息 + #[serde(rename="ScanCodeInfo")] + pub scan_code_info: ScanCodeInfo, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ScanCodeInfo { + /// 扫描类型,一般是qrcode + #[serde(rename = "ScanType")] + pub scan_type: String, + /// 扫描结果,即二维码对应的字符串信息 + #[serde(rename = "ScanResult")] + pub scan_result: String, +} + + + +/// 扫码推事件且弹出“消息接收中”提示框的事件推送 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpMenuScanCodeWaitMsgEvent { + #[serde(rename = "ToUserName")] + pub target: String, + #[serde(rename = "FromUserName")] + pub source: String, + # [serde(rename = "CreateTime")] + pub create_time: i64, + /// 事件类型:click + #[serde(rename="Event")] + pub event: String, + #[serde(rename="MsgType")] + pub msg_type: String, + /// 事件KEY值,与自定义菜单接口中KEY值对应 + #[serde(rename="EventKey")] + pub event_key: String, + /// 企业应用的id,整型。可在应用的设置页面查看 + #[serde(rename="AgentID")] + pub agent_id: i64, + /// 扫描信息 + #[serde(rename="ScanCodeInfo")] + pub scan_code_info: ScanCodeInfo, +} + + +/// 弹出系统拍照发图的事件推送 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpMenuPicSysPhotoEvent { + #[serde(rename = "ToUserName")] + pub target: String, + #[serde(rename = "FromUserName")] + pub source: String, + # [serde(rename = "CreateTime")] + pub create_time: i64, + /// 事件类型:click + #[serde(rename="Event")] + pub event: String, + #[serde(rename="MsgType")] + pub msg_type: String, + /// 事件KEY值,与自定义菜单接口中KEY值对应 + #[serde(rename="EventKey")] + pub event_key: String, + /// 企业应用的id,整型。可在应用的设置页面查看 + #[serde(rename="AgentID")] + pub agent_id: i64, + /// 发送的图片信息 + #[serde(rename="SendPicsInfo")] + pub send_pics_info: SendPicsInfo, +} + + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SendPicsInfo { + /// 发送的图片数量 + #[serde(rename = "Count")] + pub count: i64, + /// 扫描结果,即二维码对应的字符串信息 + #[serde(rename = "PicList")] + pub pic_list: Option, +} + + +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct PicList { + #[serde(rename = "item")] + items: Vec +} + +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct PicItem { + #[serde(rename = "PicMd5Sum")] + pic_md5_sum: String, +} + + +/// 弹出拍照或者相册发图的事件推送 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpMenuPicPhotoOrAlbumEvent { + #[serde(rename = "ToUserName")] + pub target: String, + #[serde(rename = "FromUserName")] + pub source: String, + # [serde(rename = "CreateTime")] + pub create_time: i64, + /// 事件类型:click + #[serde(rename="Event")] + pub event: String, + #[serde(rename="MsgType")] + pub msg_type: String, + /// 事件KEY值,与自定义菜单接口中KEY值对应 + #[serde(rename="EventKey")] + pub event_key: String, + /// 企业应用的id,整型。可在应用的设置页面查看 + #[serde(rename="AgentID")] + pub agent_id: i64, + /// 发送的图片信息 + #[serde(rename="SendPicsInfo")] + pub send_pics_info: SendPicsInfo, +} + + + +/// 弹出微信相册发图器的事件推送 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpMenuPicWeixinEvent { + #[serde(rename = "ToUserName")] + pub target: String, + #[serde(rename = "FromUserName")] + pub source: String, + # [serde(rename = "CreateTime")] + pub create_time: i64, + /// 事件类型:click + #[serde(rename="Event")] + pub event: String, + #[serde(rename="MsgType")] + pub msg_type: String, + /// 事件KEY值,与自定义菜单接口中KEY值对应 + #[serde(rename="EventKey")] + pub event_key: String, + /// 企业应用的id,整型。可在应用的设置页面查看 + #[serde(rename="AgentID")] + pub agent_id: i64, + /// 发送的图片信息 + #[serde(rename="SendPicsInfo")] + pub send_pics_info: SendPicsInfo, +} + + + +/// 弹出地理位置选择器的事件推送 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpMenuLocationSelectEvent { + #[serde(rename = "ToUserName")] + pub target: String, + #[serde(rename = "FromUserName")] + pub source: String, + # [serde(rename = "CreateTime")] + pub create_time: i64, + /// 事件类型:click + #[serde(rename="Event")] + pub event: String, + #[serde(rename="MsgType")] + pub msg_type: String, + /// 事件KEY值,与自定义菜单接口中KEY值对应 + #[serde(rename="EventKey")] + pub event_key: String, + /// 企业应用的id,整型。可在应用的设置页面查看 + #[serde(rename="AgentID")] + pub agent_id: i64, + /// 发送的图片信息 + #[serde(rename="SendLocationInfo")] + pub send_location_info: SendLocationInfo, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SendLocationInfo { + #[serde(rename="Location_X")] + pub location_x: f64, + #[serde(rename="Location_Y")] + pub location_y: f64, + #[serde(rename="Scale")] + pub scale: usize, + #[serde(rename="Label")] + pub label: String, + /// POI的名字,可能为空 + #[serde(rename="Poiname")] + pub poiname: Option, +} diff --git a/src/wechat/cp/events/mod.rs b/src/wechat/cp/events/mod.rs new file mode 100644 index 0000000..cdebe2c --- /dev/null +++ b/src/wechat/cp/events/mod.rs @@ -0,0 +1,40 @@ +//! 事件格式 +//! 开启接收消息模式后,可以配置接收事件消息。 +//! 当企业成员通过企业微信APP或微信插件(原企业号)触发进入应用、上报地理位置、点击菜单等事件时,企业微信会将这些事件消息发送给企业后台。 +//! 如何接收消息已经在使用接收消息说明,本小节是对事件消息结构体的说明。 +//! +//! 注:以下出现的数据包仅是接收的消息包中的Encrypt参数解密后的内容说明 +//! +mod subscribe; +mod unsubscribe; +mod enter_agent; +mod location; +mod batch_job_result; +mod contact_change; +mod menu; +mod approval; +mod share_agent; +mod share_chain; +mod template_card; +mod ticket; +mod auth; +mod permanent_code; +mod app_admin; +mod tp_contact_change; + +pub use subscribe::CpSubscribeEvent; +pub use unsubscribe::CpUnsubscribeEvent; +pub use enter_agent::CpEnterAgentEvent; +pub use location::CpLocationEvent; +pub use batch_job_result::CpBatchJobResultEvent; +pub use contact_change::*; +pub use menu::*; +pub use approval::*; +pub use share_agent::*; +pub use share_chain::*; +pub use ticket::*; +pub use template_card::*; +pub use auth::*; +pub use permanent_code::*; +pub use app_admin::*; +pub use tp_contact_change::*; \ No newline at end of file diff --git a/src/wechat/cp/events/permanent_code.rs b/src/wechat/cp/events/permanent_code.rs new file mode 100644 index 0000000..25008c5 --- /dev/null +++ b/src/wechat/cp/events/permanent_code.rs @@ -0,0 +1,15 @@ + +use serde::{Serialize, Deserialize}; + +/// 重置永久授权码通知 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpPermanentCodeEvent { + #[serde(rename="SuiteId")] + pub suite_id: String, + #[serde(rename="InfoType")] + pub info_type: String, + #[serde(rename="TimeStamp")] + pub time: i64, + #[serde(rename="AuthCode")] + pub auth_code: String, +} \ No newline at end of file diff --git a/src/wechat/cp/events/share_agent.rs b/src/wechat/cp/events/share_agent.rs new file mode 100644 index 0000000..a3af19f --- /dev/null +++ b/src/wechat/cp/events/share_agent.rs @@ -0,0 +1,21 @@ +use serde::{Serialize, Deserialize}; + +/// 企业互联共享应用事件回调 +/// 本事件触发时机为: +/// 1. 上级企业把自建应用共享给下级企业使用 +/// 2. 上级企业把下级企业从共享应用中移除 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpShareAgentChangeEvent { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="AgentID")] + pub agent_id: i64, +} diff --git a/src/wechat/cp/events/share_chain.rs b/src/wechat/cp/events/share_chain.rs new file mode 100644 index 0000000..42e7201 --- /dev/null +++ b/src/wechat/cp/events/share_chain.rs @@ -0,0 +1,21 @@ +use serde::{Serialize, Deserialize}; + +/// 上下游共享应用事件回调 +/// 本事件触发时机为: +/// 1. 上级企业把自建应用共享给下级企业使用 +/// 2. 上级企业把下级企业从共享应用中移除 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpShareChainChangeEvent { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="AgentID")] + pub agent_id: i64, +} diff --git a/src/wechat/cp/events/subscribe.rs b/src/wechat/cp/events/subscribe.rs new file mode 100644 index 0000000..5e8cc14 --- /dev/null +++ b/src/wechat/cp/events/subscribe.rs @@ -0,0 +1,45 @@ +use serde::{Serialize, Deserialize}; + +/// 成员关注及取消关注事件 +/// 成员已经加入企业,管理员添加成员到应用可见范围(或移除可见范围)时 +/// 成员已经在应用可见范围,成员加入(或退出)企业时 + + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpSubscribeEvent { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="AgentID")] + pub agent_id: i64, +} + +#[cfg(test)] +mod tests { + use crate::XmlMessageParser; + use super::CpSubscribeEvent; + + #[test] + fn test_from_xml() { + let xml = " + + + 123456789 + + + + "; + let msg = CpSubscribeEvent::from_xml(xml).unwrap(); + + assert_eq!("fromUser", &msg.source); + assert_eq!("toUser", &msg.target); + assert_eq!("click", &msg.event); + } +} \ No newline at end of file diff --git a/src/wechat/cp/events/template_card.rs b/src/wechat/cp/events/template_card.rs new file mode 100644 index 0000000..5080340 --- /dev/null +++ b/src/wechat/cp/events/template_card.rs @@ -0,0 +1,85 @@ +use serde::{Serialize, Deserialize}; + +/// 模板卡片事件推送 +/// 应用下发的模板卡片消息,用户点击按钮之后触发此事件 +/// 应用收到该事件之后,可以响应回复模板卡片更新消息 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpTemplateCardEvent { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="AgentID")] + pub agent_id: i64, + #[serde(rename="TaskId")] + pub task_id: Option, + #[serde(rename="CardType")] + pub card_type: Option, + /// 用于调用更新卡片接口的ResponseCode,24小时内有效,且只能使用一次 + #[serde(rename="ResponseCode")] + pub response_code: Option, + #[serde(rename="SelectedItems")] + pub selected_items: Option, +} + +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct SelectedItems { + #[serde(rename = "SelectedItem")] + items: Vec +} + +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct SelectedItem { + /// 问题的key值 + #[serde(rename = "QuestionKey")] + question_key: Option, + /// 对应问题的选项列表 + #[serde(rename = "OptionIds")] + option_ids: Option, +} + +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct OptionIds { + #[serde(rename = "OptionId")] + items: Option +} + +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct OptionId { + #[serde(rename = "QuestionKey")] + question_key: String, + #[serde(rename = "OptionIds")] + option_ids: OptionIds, +} + + + +/// 通用模板卡片右上角菜单事件推送 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpTemplateCardMenuEvent { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="AgentID")] + pub agent_id: i64, + #[serde(rename="TaskId")] + pub task_id: Option, + #[serde(rename="CardType")] + pub card_type: Option, + /// 用于调用更新卡片接口的ResponseCode,24小时内有效,且只能使用一次 + #[serde(rename="ResponseCode")] + pub response_code: Option, +} \ No newline at end of file diff --git a/src/wechat/cp/events/ticket.rs b/src/wechat/cp/events/ticket.rs new file mode 100644 index 0000000..205f904 --- /dev/null +++ b/src/wechat/cp/events/ticket.rs @@ -0,0 +1,14 @@ + +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpTicketEvent { + #[serde(rename="SuiteId")] + pub suite_id: String, + #[serde(rename="InfoType")] + pub info_type: String, + #[serde(rename="TimeStamp")] + pub time: i64, + #[serde(rename="SuiteTicket")] + pub suite_ticket: String, +} \ No newline at end of file diff --git a/src/wechat/cp/events/tp_contact_change.rs b/src/wechat/cp/events/tp_contact_change.rs new file mode 100644 index 0000000..480934c --- /dev/null +++ b/src/wechat/cp/events/tp_contact_change.rs @@ -0,0 +1,313 @@ +use serde::{Deserialize, Serialize}; + + +/// 新增成员事件 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpTpContactCreateUserEvent { + /// 第三方应用ID + #[serde(rename="SuiteId")] + pub suite_id: String, + #[serde(rename="InfoType")] + pub info_type: String, + #[serde(rename="TimeStamp")] + pub time: i64, + /// 授权企业的CorpID + #[serde(rename="AuthCorpId")] + pub auth_corp_id: String, + #[serde(rename="ChangeType")] + pub change_type: String, + /// 成员UserID + #[serde(rename="UserID")] + pub user_id: String, + /// 成员名称;代开发自建应用需要管理员授权才返回 + #[serde(rename="Name")] + pub name: Option, + /// 成员部门列表,仅返回该应用有查看权限的部门id + #[serde(rename="Department")] + pub department: String, + /// 主部门 + #[serde(rename="MainDepartment")] + pub main_department: i64, + /// 表示所在部门是否为部门负责人,0-否,1-是,顺序与Department字段的部门逐一对应。上游共享的应用不返回该字段 + #[serde(rename="IsLeaderInDept")] + pub is_leader_in_dept: Option, + /// 直属上级UserID,最多5个。代开发的自建应用和上游共享的应用不返回该字段 + #[serde(rename="DirectLeader")] + pub direct_leader: Option>, + /// 手机号码,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Mobile")] + pub mobile: Option, + /// 性别。0表示未定义,1表示男性,2表示女性。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段。注:不可获取指返回值0 + #[serde(rename="Gender")] + pub gender: Option, + /// 职位信息。长度为0~64个字节;代开发自建应用需要管理员授权才返回。上游共享的应用不返回该字段 + #[serde(rename="Position")] + pub position: Option, + /// 邮箱,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Email")] + pub email: String, + /// 企业邮箱,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="BizMail")] + pub biz_mail: Option, + /// 头像url。 注:如果要获取小图将url最后的”/0”改成”/100”即可。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Avatar")] + pub avatar: Option, + /// 座机;代开发自建应用需要管理员授权才返回。上游共享的应用不返回该字段 + #[serde(rename="Telephone")] + pub telephone: Option, + /// 地址。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Address")] + pub address: Option, + /// 成员别名。上游共享的应用不返回该字段 + #[serde(rename="Alias")] + pub alias: Option, + #[serde(rename="ExtAttr")] + pub ext_attrs: Option, +} + + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpExtAttrItemText { + #[serde(rename = "Value")] + pub value: String, +} +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpExtAttrItemWeb { + #[serde(rename="Title")] + pub title: String, + #[serde(rename="Url")] + pub url: String, +} + + + + + +/// 更新成员事件 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpTpContactUpdateUserEvent { + /// 第三方应用ID + #[serde(rename="SuiteId")] + pub suite_id: String, + #[serde(rename="InfoType")] + pub info_type: String, + #[serde(rename="TimeStamp")] + pub time: i64, + /// 授权企业的CorpID + #[serde(rename="AuthCorpId")] + pub auth_corp_id: String, + #[serde(rename="ChangeType")] + pub change_type: String, + /// 成员UserID + #[serde(rename="UserID")] + pub user_id: String, + /// 全局唯一。对于同一个服务商,不同应用获取到企业内同一个成员的OpenUserID是相同的,最多64个字节。 + #[serde(rename="OpenUserID")] + pub open_user_id: String, + /// 新的UserID,变更时推送(userid由系统生成时可更改一次) + #[serde(rename="NewUserID")] + pub new_user_id: String, + /// 成员名称;代开发自建应用需要管理员授权才返回 + #[serde(rename="Name")] + pub name: Option, + /// 成员部门列表,仅返回该应用有查看权限的部门id + #[serde(rename="Department")] + pub department: String, + /// 主部门 + #[serde(rename="MainDepartment")] + pub main_department: i64, + /// 表示所在部门是否为部门负责人,0-否,1-是,顺序与Department字段的部门逐一对应。上游共享的应用不返回该字段 + #[serde(rename="IsLeaderInDept")] + pub is_leader_in_dept: Option, + /// 职位信息。长度为0~64个字节;代开发自建应用需要管理员授权才返回。上游共享的应用不返回该字段 + #[serde(rename="Position")] + pub position: Option, + /// 手机号码,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Mobile")] + pub mobile: Option, + /// 性别。0表示未定义,1表示男性,2表示女性。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段。注:不可获取指返回值0 + #[serde(rename="Gender")] + pub gender: Option, + /// 邮箱,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Email")] + pub email: String, + /// 激活状态:1=已激活 2=已禁用 4=未激活 已激活代表已激活企业微信或已关注微信插件(原企业号)5=成员退出 + #[serde(rename="Status")] + pub status: i64, + /// 头像url。 注:如果要获取小图将url最后的”/0”改成”/100”即可。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Avatar")] + pub avatar: Option, + /// 成员别名。上游共享的应用不返回该字段 + #[serde(rename="Alias")] + pub alias: Option, + /// 座机;代开发自建应用需要管理员授权才返回。上游共享的应用不返回该字段 + #[serde(rename="Telephone")] + pub telephone: Option, + /// 地址。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="Address")] + pub address: Option, + /// 直属上级UserID,最多5个。代开发的自建应用和上游共享的应用不返回该字段 + #[serde(rename="DirectLeader")] + pub direct_leader: Option, + /// 企业邮箱,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + #[serde(rename="BizMail")] + pub biz_mail: Option, + #[serde(rename="ExtAttr")] + pub ext_attrs: Option, +} + +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct ExtAttrs { + #[serde(rename = "Item")] + items: Vec +} + +#[derive(Debug, Serialize, Deserialize,Clone)] +pub struct ExtAttrItem { + #[serde(rename = "Name")] + name: String, + #[serde(rename = "Type")] + attr_type: u32, + #[serde(rename="Text")] + pub text: Option, + #[serde(rename="Web")] + pub web: Option, +} + + + +/// 删除成员事件 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpTpContactDeleteUserEvent { + /// 第三方应用ID + #[serde(rename="SuiteId")] + pub suite_id: String, + #[serde(rename="InfoType")] + pub info_type: String, + #[serde(rename="TimeStamp")] + pub time: i64, + /// 授权企业的CorpID + #[serde(rename="AuthCorpId")] + pub auth_corp_id: String, + #[serde(rename="ChangeType")] + pub change_type: String, + /// 全局唯一。对于同一个服务商,不同应用获取到企业内同一个成员的OpenUserID是相同的,最多64个字节。 + #[serde(rename="OpenUserID")] + pub open_user_id: String, + /// 成员UserID + #[serde(rename="UserID")] + pub user_id: String, +} + + + + +/// 新增部门事件 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpTpContactCreatePartyEvent { + /// 第三方应用ID + #[serde(rename="SuiteId")] + pub suite_id: String, + #[serde(rename="InfoType")] + pub info_type: String, + #[serde(rename="TimeStamp")] + pub time: i64, + /// 授权企业的CorpID + #[serde(rename="AuthCorpId")] + pub auth_corp_id: String, + #[serde(rename="ChangeType")] + pub change_type: String, + /// 部门Id + #[serde(rename="Id")] + pub id: i32, + /// 父部门id + #[serde(rename="ParentId")] + pub parent_id: i32, + /// 部门名称 + #[serde(rename="Name")] + pub name: String, + /// 部门排序 + #[serde(rename="Order")] + pub order: i32, +} + + +/// 更新部门事件 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpTpContactUpdatePartyEvent { + /// 第三方应用ID + #[serde(rename="SuiteId")] + pub suite_id: String, + #[serde(rename="InfoType")] + pub info_type: String, + #[serde(rename="TimeStamp")] + pub time: i64, + /// 授权企业的CorpID + #[serde(rename="AuthCorpId")] + pub auth_corp_id: String, + #[serde(rename="ChangeType")] + pub change_type: String, + /// 部门Id + #[serde(rename="Id")] + pub id: i32, + #[serde(rename="ParentId")] + pub parent_id: i32, + /// 部门名称 + #[serde(rename="Name")] + pub name: String, +} + + + +/// 删除部门事件 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpTpContactDeletePartyEvent { + /// 第三方应用ID + #[serde(rename="SuiteId")] + pub suite_id: String, + #[serde(rename="InfoType")] + pub info_type: String, + #[serde(rename="TimeStamp")] + pub time: i64, + /// 授权企业的CorpID + #[serde(rename="AuthCorpId")] + pub auth_corp_id: String, + #[serde(rename="ChangeType")] + pub change_type: String, + /// 部门Id + #[serde(rename="Id")] + pub id: i32, +} + +/// 标签成员变更事件 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpTpContactUpdateTagEvent { + /// 第三方应用ID + #[serde(rename="SuiteId")] + pub suite_id: String, + #[serde(rename="InfoType")] + pub info_type: String, + #[serde(rename="TimeStamp")] + pub time: i64, + /// 授权企业的CorpID + #[serde(rename="AuthCorpId")] + pub auth_corp_id: String, + #[serde(rename="ChangeType")] + pub change_type: String, + /// 标签Id + #[serde(rename="TagId")] + pub tag_id: i32, + /// 标签中新增的成员userid列表,用逗号分隔 + #[serde(rename="AddUserItems")] + pub add_users: Option, + /// 标签中删除的成员userid列表,用逗号分隔 + #[serde(rename="DelUserItems")] + pub delete_users: Option, + /// 标签中新增的部门id列表,用逗号分隔 + #[serde(rename="AddPartyItems")] + pub add_party: Option, + /// 标签中删除的部门id列表,用逗号分隔 + #[serde(rename="DelPartyItems")] + pub delete_party: Option, +} + diff --git a/src/wechat/cp/events/unsubscribe.rs b/src/wechat/cp/events/unsubscribe.rs new file mode 100644 index 0000000..f1fce4c --- /dev/null +++ b/src/wechat/cp/events/unsubscribe.rs @@ -0,0 +1,40 @@ + +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpUnsubscribeEvent { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="AgentID")] + pub agent_id: i64, +} + +#[cfg(test)] +mod tests { + use crate::wechat::cp::events::unsubscribe::CpUnsubscribeEvent; + use crate::XmlMessageParser; + + #[test] + fn test_from_xml() { + let xml = "\ + \ + \ + 123456789\ + \ + \ + "; + let msg = CpUnsubscribeEvent::from_xml(xml).unwrap(); + + assert_eq!("fromUser", &msg.source); + assert_eq!("toUser", &msg.target); + assert_eq!("unsubscribe", &msg.event); + } +} \ No newline at end of file diff --git a/src/wechat/cp/messages/image.rs b/src/wechat/cp/messages/image.rs new file mode 100644 index 0000000..b760d3c --- /dev/null +++ b/src/wechat/cp/messages/image.rs @@ -0,0 +1,46 @@ + +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpImageMessage { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="MediaId")] + pub media_id: String, + #[serde(rename="PicUrl")] + pub image: String, + #[serde(rename="AgentID")] + pub agent_id: i64, +} + +#[cfg(test)] +mod tests { + use crate::XmlMessageParser; + use super::CpImageMessage; + + #[test] + fn test_from_xml() { + let xml = "\ + \ + \ + 1348831860\ + \ + \ + \ + 1234567890123456\ + "; + let msg = CpImageMessage::from_xml(xml).unwrap(); + + assert_eq!("fromUser", &msg.source); + assert_eq!("toUser", &msg.target); + assert_eq!(1234567890123456, msg.id); + assert_eq!("media_id", &msg.media_id); + assert_eq!("this is a url", &msg.image); + } +} diff --git a/src/wechat/cp/messages/link.rs b/src/wechat/cp/messages/link.rs new file mode 100644 index 0000000..474b698 --- /dev/null +++ b/src/wechat/cp/messages/link.rs @@ -0,0 +1,51 @@ + +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpLinkMessage { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="Title")] + pub title: String, + #[serde(rename="Description")] + pub description: String, + #[serde(rename="Url")] + pub url: String, + #[serde(rename="AgentID")] + pub agent_id: i64, +} + + +#[cfg(test)] +mod tests { + use crate::XmlMessageParser; + use super::CpLinkMessage; + + #[test] + fn test_from_xml() { + let xml = "\ + \ + \ + 1348831860\ + \ + <![CDATA[公众平台官网链接]]>\ + \ + \ + 1234567890123456\ + "; + let msg = CpLinkMessage::from_xml(xml).unwrap(); + + assert_eq!("fromUser", &msg.source); + assert_eq!("toUser", &msg.target); + assert_eq!(1234567890123456, msg.id); + assert_eq!("公众平台官网链接", &msg.title); + assert_eq!("公众平台官网链接", &msg.description); + assert_eq!("url", &msg.url); + } +} diff --git a/src/wechat/cp/messages/location.rs b/src/wechat/cp/messages/location.rs new file mode 100644 index 0000000..f2b4746 --- /dev/null +++ b/src/wechat/cp/messages/location.rs @@ -0,0 +1,54 @@ +use serde::{Serialize, Deserialize}; + + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpLocationMessage { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="Location_X")] + pub location_x: f64, + #[serde(rename="Location_Y")] + pub location_y: f64, + #[serde(rename="Scale")] + pub scale: usize, + #[serde(rename="Label")] + pub label: String, + #[serde(rename="AgentID")] + pub agent_id: i64, +} + +#[cfg(test)] +mod tests { + use crate::XmlMessageParser; + use super::CpLocationMessage; + + #[test] + fn test_from_xml() { + let xml = "\ + \ + \ + 1348831860\ + \ + 23.134521\ + 113.358803 + 20\ + + 1234567890123456\ + "; + let msg = CpLocationMessage::from_xml(xml).unwrap(); + + assert_eq!("fromUser", &msg.source); + assert_eq!("toUser", &msg.target); + assert_eq!(1234567890123456, msg.id); + assert_eq!(23, msg.location_x as usize); + assert_eq!(113, msg.location_y as usize); + assert_eq!(20, msg.scale); + assert_eq!("位置信息", &msg.label); + } +} diff --git a/src/wechat/cp/messages/mod.rs b/src/wechat/cp/messages/mod.rs new file mode 100644 index 0000000..d2dc2e6 --- /dev/null +++ b/src/wechat/cp/messages/mod.rs @@ -0,0 +1,156 @@ +//! 消息格式 +//! 开启接收消息模式后,企业成员在企业微信应用里发送消息时,企业微信会将消息同步到企业应用的后台。 +//! 如何接收消息已经在使用接收消息说明,本小节是对普通消息结构体的说明。 +//! 消息类型支持:文本、图片、语音、视频、位置以及链接信息。 +//! 注:以下出现的xml包仅是接收的消息包中的Encrypt参数解密后的内容说明 +mod text; +mod image; +mod voice; +mod video; +mod location; +mod link; +mod unknown; + +use crate::{CpAppAdminChangeEvent, CpAuthCancelEvent, CpAuthChangeEvent, CpAuthCreateEvent, CpBatchJobResultEvent, CpContactCreatePartyEvent, CpContactCreateUserEvent, CpContactDeletePartyEvent, CpContactDeleteUserEvent, CpContactUpdatePartyEvent, CpContactUpdateTagEvent, CpContactUpdateUserEvent, CpEnterAgentEvent, CpLocationEvent, CpMenuClickEvent, CpMenuLocationSelectEvent, CpMenuPicPhotoOrAlbumEvent, CpMenuPicSysPhotoEvent, CpMenuPicWeixinEvent, CpMenuScanCodePushEvent, CpMenuScanCodeWaitMsgEvent, CpMenuViewEvent, CpOpenApprovalChangeEvent, CpPermanentCodeEvent, CpShareAgentChangeEvent, CpShareChainChangeEvent, CpSubscribeEvent, CpTemplateCardEvent, CpTemplateCardMenuEvent, CpTicketEvent, CpTpContactCreatePartyEvent, CpTpContactCreateUserEvent, CpTpContactDeletePartyEvent, CpTpContactDeleteUserEvent, CpTpContactUpdatePartyEvent, CpTpContactUpdateTagEvent, CpTpContactUpdateUserEvent, CpUnsubscribeEvent, LabradorResult, parse_cp_message}; +// export Message types +pub use self::text::CpTextMessage; +pub use self::image::CpImageMessage; +pub use self::voice::CpVoiceMessage; +pub use self::video::CpVideoMessage; +pub use self::location::CpLocationMessage; +pub use self::link::CpLinkMessage; +pub use self::unknown::CpUnknownMessage; + +// an enum or messages and events +#[allow(unused)] +#[derive(Debug, Clone)] +pub enum CpMessage { + TextMessage(CpTextMessage), + ImageMessage(CpImageMessage), + VoiceMessage(CpVoiceMessage), + VideoMessage(CpVideoMessage), + LocationMessage(CpLocationMessage), + LinkMessage(CpLinkMessage), + TicketEvent(CpTicketEvent), + AuthChangeEvent(CpAuthChangeEvent), + AuthCreateEvent(CpAuthCreateEvent), + AuthCancelEvent(CpAuthCancelEvent), + PermanentCodeEvent(CpPermanentCodeEvent), + AppAdminChangeEvent(CpAppAdminChangeEvent), + UnknownMessage(CpUnknownMessage), + LocationEvent(CpLocationEvent), + OpenApprovalChangeEvent(CpOpenApprovalChangeEvent), + BatchJobResultEvent(CpBatchJobResultEvent), + ContactCreateUserEvent(CpContactCreateUserEvent), + ContactUpdateUserEvent(CpContactUpdateUserEvent), + ContactDeleteUserEvent(CpContactDeleteUserEvent), + ContactCreatePartyEvent(CpContactCreatePartyEvent), + ContactUpdatePartyEvent(CpContactUpdatePartyEvent), + ContactDeletePartyEvent(CpContactDeletePartyEvent), + ContactUpdateTagEvent(CpContactUpdateTagEvent), + TpContactCreateUserEvent(CpTpContactCreateUserEvent), + TpContactUpdateUserEvent(CpTpContactUpdateUserEvent), + TpContactDeleteUserEvent(CpTpContactDeleteUserEvent), + TpContactCreatePartyEvent(CpTpContactCreatePartyEvent), + TpContactUpdatePartyEvent(CpTpContactUpdatePartyEvent), + TpContactDeletePartyEvent(CpTpContactDeletePartyEvent), + TpContactUpdateTagEvent(CpTpContactUpdateTagEvent), + EnterAgentEvent(CpEnterAgentEvent), + MenuClickEvent(CpMenuClickEvent), + MenuViewEvent(CpMenuViewEvent), + MenuPicWeixinEvent(CpMenuPicWeixinEvent), + MenuLocationSelectEvent(CpMenuLocationSelectEvent), + MenuPicSysPhotoEvent(CpMenuPicSysPhotoEvent), + MenuScanCodePushEvent(CpMenuScanCodePushEvent), + MenuPicPhotoOrAlbumEvent(CpMenuPicPhotoOrAlbumEvent), + MenuScanCodeWaitMsgEvent(CpMenuScanCodeWaitMsgEvent), + ShareAgentChangeEvent(CpShareAgentChangeEvent), + ShareChainChangeEvent(CpShareChainChangeEvent), + SubscribeEvent(CpSubscribeEvent), + UnsubscribeEvent(CpUnsubscribeEvent), + TemplateCardEvent(CpTemplateCardEvent), + TemplateCardMenuEvent(CpTemplateCardMenuEvent), +} + +#[allow(unused)] +impl CpMessage { + pub fn parse>(xml: S) -> LabradorResult { + parse_cp_message(xml.as_ref()) + } + + pub fn get_source(&self) -> String { + match *self { + CpMessage::TextMessage(ref msg) => msg.source.to_string(), + CpMessage::ImageMessage(ref msg) => msg.source.to_string(), + CpMessage::VoiceMessage(ref msg) => msg.source.to_string(), + CpMessage::VideoMessage(ref msg) => msg.source.to_string(), + CpMessage::LocationMessage(ref msg) => msg.source.to_string(), + CpMessage::LinkMessage(ref msg) => msg.source.to_string(), + CpMessage::UnknownMessage(ref msg) => msg.source.to_string(), + CpMessage::LocationEvent(ref msg) => msg.source.to_string(), + CpMessage::OpenApprovalChangeEvent(ref msg) => msg.source.to_string(), + CpMessage::BatchJobResultEvent(ref msg) => msg.source.to_string(), + CpMessage::ContactCreateUserEvent(ref msg) => msg.source.to_string(), + CpMessage::ContactUpdateUserEvent(ref msg) => msg.source.to_string(), + CpMessage::ContactDeleteUserEvent(ref msg) => msg.source.to_string(), + CpMessage::ContactCreatePartyEvent(ref msg) => msg.source.to_string(), + CpMessage::ContactUpdatePartyEvent(ref msg) => msg.source.to_string(), + CpMessage::ContactDeletePartyEvent(ref msg) => msg.source.to_string(), + CpMessage::ContactUpdateTagEvent(ref msg) => msg.source.to_string(), + CpMessage::EnterAgentEvent(ref msg) => msg.source.to_string(), + CpMessage::MenuClickEvent(ref msg) => msg.source.to_string(), + CpMessage::MenuViewEvent(ref msg) => msg.source.to_string(), + CpMessage::ShareAgentChangeEvent(ref msg) => msg.source.to_string(), + CpMessage::ShareChainChangeEvent(ref msg) => msg.source.to_string(), + CpMessage::SubscribeEvent(ref msg) => msg.source.to_string(), + CpMessage::UnsubscribeEvent(ref msg) => msg.source.to_string(), + CpMessage::TemplateCardEvent(ref msg) => msg.source.to_string(), + CpMessage::TemplateCardMenuEvent(ref msg) => msg.source.to_string(), + CpMessage::MenuPicWeixinEvent(ref msg) => msg.source.to_string(), + CpMessage::MenuLocationSelectEvent(ref msg) => msg.source.to_string(), + CpMessage::MenuPicSysPhotoEvent(ref msg) => msg.source.to_string(), + CpMessage::MenuScanCodePushEvent(ref msg) => msg.source.to_string(), + CpMessage::MenuPicPhotoOrAlbumEvent(ref msg) => msg.source.to_string(), + CpMessage::MenuScanCodeWaitMsgEvent(ref msg) => msg.source.to_string(), + _ => "".to_string() + } + } + + pub fn get_target(&self) -> String { + match *self { + CpMessage::TextMessage(ref msg) => msg.target.to_string(), + CpMessage::ImageMessage(ref msg) => msg.target.to_string(), + CpMessage::VoiceMessage(ref msg) => msg.target.to_string(), + CpMessage::VideoMessage(ref msg) => msg.target.to_string(), + CpMessage::LocationMessage(ref msg) => msg.target.to_string(), + CpMessage::LinkMessage(ref msg) => msg.target.to_string(), + CpMessage::UnknownMessage(ref msg) => msg.target.to_string(), + CpMessage::LocationEvent(ref msg) => msg.target.to_string(), + CpMessage::OpenApprovalChangeEvent(ref msg) => msg.target.to_string(), + CpMessage::BatchJobResultEvent(ref msg) => msg.target.to_string(), + CpMessage::ContactCreateUserEvent(ref msg) => msg.target.to_string(), + CpMessage::ContactUpdateUserEvent(ref msg) => msg.target.to_string(), + CpMessage::ContactDeleteUserEvent(ref msg) => msg.target.to_string(), + CpMessage::ContactCreatePartyEvent(ref msg) => msg.target.to_string(), + CpMessage::ContactUpdatePartyEvent(ref msg) => msg.target.to_string(), + CpMessage::ContactDeletePartyEvent(ref msg) => msg.target.to_string(), + CpMessage::ContactUpdateTagEvent(ref msg) => msg.target.to_string(), + CpMessage::EnterAgentEvent(ref msg) => msg.target.to_string(), + CpMessage::MenuClickEvent(ref msg) => msg.target.to_string(), + CpMessage::MenuViewEvent(ref msg) => msg.target.to_string(), + CpMessage::ShareAgentChangeEvent(ref msg) => msg.target.to_string(), + CpMessage::ShareChainChangeEvent(ref msg) => msg.target.to_string(), + CpMessage::SubscribeEvent(ref msg) => msg.target.to_string(), + CpMessage::UnsubscribeEvent(ref msg) => msg.target.to_string(), + CpMessage::TemplateCardEvent(ref msg) => msg.target.to_string(), + CpMessage::TemplateCardMenuEvent(ref msg) => msg.target.to_string(), + CpMessage::MenuPicWeixinEvent(ref msg) => msg.target.to_string(), + CpMessage::MenuLocationSelectEvent(ref msg) => msg.target.to_string(), + CpMessage::MenuPicSysPhotoEvent(ref msg) => msg.target.to_string(), + CpMessage::MenuScanCodePushEvent(ref msg) => msg.target.to_string(), + CpMessage::MenuPicPhotoOrAlbumEvent(ref msg) => msg.target.to_string(), + CpMessage::MenuScanCodeWaitMsgEvent(ref msg) => msg.target.to_string(), + _ => "".to_string() + } + } +} diff --git a/src/wechat/cp/messages/text.rs b/src/wechat/cp/messages/text.rs new file mode 100644 index 0000000..4f03a65 --- /dev/null +++ b/src/wechat/cp/messages/text.rs @@ -0,0 +1,43 @@ + +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpTextMessage { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="Content")] + pub content: String, + #[serde(rename="AgentID")] + pub agent_id: i64, +} + +#[cfg(test)] +mod tests { + use crate::XmlMessageParser; + use super::CpTextMessage; + + #[test] + fn test_from_xml() { + let xml = "\ + \ + \ + 1348831860\ + \ + \ + 1234567890123456\ + 1\ + "; + let msg = CpTextMessage::from_xml(xml).unwrap(); + + assert_eq!("fromUser", &msg.source); + assert_eq!("toUser", &msg.target); + assert_eq!(1234567890123456, msg.id); + assert_eq!("this is a test", &msg.content); + } +} diff --git a/src/wechat/cp/messages/unknown.rs b/src/wechat/cp/messages/unknown.rs new file mode 100644 index 0000000..50ae7a5 --- /dev/null +++ b/src/wechat/cp/messages/unknown.rs @@ -0,0 +1,16 @@ + +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpUnknownMessage { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="AgentID")] + pub agent_id: i64, +} \ No newline at end of file diff --git a/src/wechat/cp/messages/video.rs b/src/wechat/cp/messages/video.rs new file mode 100644 index 0000000..9d83fc7 --- /dev/null +++ b/src/wechat/cp/messages/video.rs @@ -0,0 +1,47 @@ + + +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpVideoMessage { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="MediaId")] + pub media_id: String, + #[serde(rename="ThumbMediaId")] + pub thumb_media_id: String, + #[serde(rename="AgentID")] + pub agent_id: i64, +} + +#[cfg(test)] +mod tests { + use crate::XmlMessageParser; + use super::CpVideoMessage; + + #[test] + fn test_from_xml() { + let xml = "\ + \ + \ + 1348831860\ + \ + \ + \ + 1234567890123456\ + "; + let msg = CpVideoMessage::from_xml(xml).unwrap(); + + assert_eq!("fromUser", &msg.source); + assert_eq!("toUser", &msg.target); + assert_eq!(1234567890123456, msg.id); + assert_eq!("media_id", &msg.media_id); + assert_eq!("thumb_media_id", &msg.thumb_media_id); + } +} diff --git a/src/wechat/cp/messages/voice.rs b/src/wechat/cp/messages/voice.rs new file mode 100644 index 0000000..b966e02 --- /dev/null +++ b/src/wechat/cp/messages/voice.rs @@ -0,0 +1,48 @@ + + +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpVoiceMessage { + #[serde(rename="FromUserName")] + pub source: String, + #[serde(rename="ToUserName")] + pub target: String, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] + pub id: i64, + #[serde(rename="MediaId")] + pub media_id: String, + #[serde(rename="Format")] + pub format: String, + #[serde(rename="AgentID")] + pub agent_id: i64, +} + + +#[cfg(test)] +mod tests { + use crate::XmlMessageParser; + use super::CpVoiceMessage; + + #[test] + fn test_from_xml() { + let xml = "\ + \ + \ + 1348831860\ + \ + \ + \ + 1234567890123456\ + "; + let msg = CpVoiceMessage::from_xml(xml).unwrap(); + + assert_eq!("fromUser", &msg.source); + assert_eq!("toUser", &msg.target); + assert_eq!(1234567890123456, msg.id); + assert_eq!("media_id", &msg.media_id); + assert_eq!("Format", &msg.format); + } +} diff --git a/src/wechat/cp/mod.rs b/src/wechat/cp/mod.rs index 7eebf86..e51b496 100644 --- a/src/wechat/cp/mod.rs +++ b/src/wechat/cp/mod.rs @@ -7,9 +7,17 @@ mod api; #[allow(unused)] mod constants; mod tp; +mod messages; +mod msg_parser; +mod events; +mod replies; pub use api::*; pub use tp::*; +pub use messages::*; +pub use replies::*; +pub use events::*; +pub use msg_parser::*; use crate::wechat::cp::constants::{ACCESS_TOKEN, CORPID, CORPSECRET}; use crate::wechat::cp::method::{WechatCpMethod}; @@ -168,8 +176,8 @@ impl WechatCpClient { /// [详情](http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN) ///
pub fn check_signature(&self, signature: &str, timestamp: i64, nonce: &str, data: &str) -> LabradorResult { - let crp = WechatCrypto::new(&self.aes_key.to_owned().unwrap_or_default()); - let _ = crp.check_signature(signature, timestamp, nonce, data, &self.token.to_owned().unwrap_or_default())?; + let crp = WechatCrypto::new(&self.aes_key.to_owned().unwrap_or_default()).token(&self.token.to_owned().unwrap_or_default()); + let _ = crp.check_signature(signature, timestamp, nonce, data)?; Ok(true) } diff --git a/src/wechat/cp/msg_parser.rs b/src/wechat/cp/msg_parser.rs new file mode 100644 index 0000000..7e5e93a --- /dev/null +++ b/src/wechat/cp/msg_parser.rs @@ -0,0 +1,78 @@ +use crate::{CpAuthCancelEvent, CpAuthChangeEvent, CpAuthCreateEvent, CpBatchJobResultEvent, CpContactCreatePartyEvent, CpContactCreateUserEvent, CpContactDeletePartyEvent, CpContactDeleteUserEvent, CpContactUpdatePartyEvent, CpContactUpdateTagEvent, CpContactUpdateUserEvent, CpEnterAgentEvent, CpImageMessage, CpLinkMessage, CpLocationEvent, CpLocationMessage, CpMenuClickEvent, CpMenuLocationSelectEvent, CpMenuPicPhotoOrAlbumEvent, CpMenuPicSysPhotoEvent, CpMenuPicWeixinEvent, CpMenuScanCodePushEvent, CpMenuScanCodeWaitMsgEvent, CpMenuViewEvent, CpMessage, CpOpenApprovalChangeEvent, CpShareAgentChangeEvent, CpShareChainChangeEvent, CpSubscribeEvent, CpTemplateCardEvent, CpTemplateCardMenuEvent, CpTextMessage, CpTicketEvent, CpTpContactCreatePartyEvent, CpTpContactCreateUserEvent, CpTpContactDeletePartyEvent, CpTpContactDeleteUserEvent, CpTpContactUpdatePartyEvent, CpTpContactUpdateTagEvent, CpTpContactUpdateUserEvent, CpUnknownMessage, CpVideoMessage, CpVoiceMessage, LabradorResult, XmlMessageParser}; + +pub fn parse_cp_message>(xml: S) -> LabradorResult { + let doc = serde_xml_rs::from_str::(xml.as_ref())?; + let msg_type = doc["MsgType"]["$value"].as_str().unwrap_or_default(); + let info_type = doc["InfoType"]["$value"].as_str().unwrap_or_default(); + match info_type { + "suite_ticket" => return Ok(CpMessage::TicketEvent(CpTicketEvent::from_xml(xml.as_ref())?)), + "create_auth" => return Ok(CpMessage::AuthCreateEvent(CpAuthCreateEvent::from_xml(xml.as_ref())?)), + "change_auth" => return Ok(CpMessage::AuthChangeEvent(CpAuthChangeEvent::from_xml(xml.as_ref())?)), + "cancel_auth" => return Ok(CpMessage::AuthCancelEvent(CpAuthCancelEvent::from_xml(xml.as_ref())?)), + "change_contact" => { + let change_type = doc["ChangeType"]["$value"].as_str().unwrap_or_default(); + match change_type { + "create_user" => return Ok(CpMessage::TpContactCreateUserEvent(CpTpContactCreateUserEvent::from_xml(xml.as_ref())?)), + "update_user" => return Ok(CpMessage::TpContactUpdateUserEvent(CpTpContactUpdateUserEvent::from_xml(xml.as_ref())?)), + "delete_user" => return Ok(CpMessage::TpContactDeleteUserEvent(CpTpContactDeleteUserEvent::from_xml(xml.as_ref())?)), + "create_party" => return Ok(CpMessage::TpContactCreatePartyEvent(CpTpContactCreatePartyEvent::from_xml(xml.as_ref())?)), + "update_party" => return Ok(CpMessage::TpContactUpdatePartyEvent(CpTpContactUpdatePartyEvent::from_xml(xml.as_ref())?)), + "delete_party" => return Ok(CpMessage::TpContactDeletePartyEvent(CpTpContactDeletePartyEvent::from_xml(xml.as_ref())?)), + "update_tag" => return Ok(CpMessage::TpContactUpdateTagEvent(CpTpContactUpdateTagEvent::from_xml(xml.as_ref())?)), + _ => {} + } + }, + _ => {} + } + let msg = match msg_type { + "text" => CpMessage::TextMessage(CpTextMessage::from_xml(xml.as_ref())?), + "image" => CpMessage::ImageMessage(CpImageMessage::from_xml(xml.as_ref())?), + "voice" => CpMessage::VoiceMessage(CpVoiceMessage::from_xml(xml.as_ref())?), + "video" => CpMessage::VideoMessage(CpVideoMessage::from_xml(xml.as_ref())?), + "location" => CpMessage::LocationMessage(CpLocationMessage::from_xml(xml.as_ref())?), + "link" => CpMessage::LinkMessage(CpLinkMessage::from_xml(xml.as_ref())?), + "event" => { + let event_str = doc["Event"]["$value"].as_str().unwrap_or_default(); + let change_type = doc["ChangeType"]["$value"].as_str().unwrap_or_default(); + parse_event(&event_str.to_lowercase(), change_type,xml.as_ref())? + }, + _ => CpMessage::UnknownMessage(CpUnknownMessage::from_xml(xml.as_ref())?), + }; + Ok(msg) +} + +fn parse_event(event: &str, change_type: &str, xml: &str) -> LabradorResult { + let msg = match event { + "location" => CpMessage::LocationEvent(CpLocationEvent::from_xml(xml)?), + "subscribe" => CpMessage::SubscribeEvent(CpSubscribeEvent::from_xml(xml)?), + "enter_agent" => CpMessage::EnterAgentEvent(CpEnterAgentEvent::from_xml(xml)?), + "batch_job_result" => CpMessage::BatchJobResultEvent(CpBatchJobResultEvent::from_xml(xml)?), + "change_contact" => { + match change_type { + "create_user" => CpMessage::ContactCreateUserEvent(CpContactCreateUserEvent::from_xml(xml)?), + "update_user" => CpMessage::ContactUpdateUserEvent(CpContactUpdateUserEvent::from_xml(xml)?), + "delete_user" => CpMessage::ContactDeleteUserEvent(CpContactDeleteUserEvent::from_xml(xml)?), + "create_party" => CpMessage::ContactCreatePartyEvent(CpContactCreatePartyEvent::from_xml(xml)?), + "update_party" => CpMessage::ContactUpdatePartyEvent(CpContactUpdatePartyEvent::from_xml(xml)?), + "delete_party" => CpMessage::ContactDeletePartyEvent(CpContactDeletePartyEvent::from_xml(xml)?), + "update_tag" => CpMessage::ContactUpdateTagEvent(CpContactUpdateTagEvent::from_xml(xml)?), + _ => CpMessage::UnknownMessage(CpUnknownMessage::from_xml(xml)?), + } + } + "click" => CpMessage::MenuClickEvent(CpMenuClickEvent::from_xml(xml)?), + "view" => CpMessage::MenuViewEvent(CpMenuViewEvent::from_xml(xml)?), + "scancode_push" => CpMessage::MenuScanCodePushEvent(CpMenuScanCodePushEvent::from_xml(xml)?), + "scancode_waitmsg" => CpMessage::MenuScanCodeWaitMsgEvent(CpMenuScanCodeWaitMsgEvent::from_xml(xml)?), + "pic_sysphoto" => CpMessage::MenuPicSysPhotoEvent(CpMenuPicSysPhotoEvent::from_xml(xml)?), + "pic_photo_or_album" => CpMessage::MenuPicPhotoOrAlbumEvent(CpMenuPicPhotoOrAlbumEvent::from_xml(xml)?), + "pic_weixin" => CpMessage::MenuPicWeixinEvent(CpMenuPicWeixinEvent::from_xml(xml)?), + "location_select" => CpMessage::MenuLocationSelectEvent(CpMenuLocationSelectEvent::from_xml(xml)?), + "open_approval_change" => CpMessage::OpenApprovalChangeEvent(CpOpenApprovalChangeEvent::from_xml(xml)?), + "share_agent_change" => CpMessage::ShareAgentChangeEvent(CpShareAgentChangeEvent::from_xml(xml)?), + "share_chain_change" => CpMessage::ShareChainChangeEvent(CpShareChainChangeEvent::from_xml(xml)?), + "template_card_event" => CpMessage::TemplateCardEvent(CpTemplateCardEvent::from_xml(xml)?), + "template_card_menu_event" => CpMessage::TemplateCardMenuEvent(CpTemplateCardMenuEvent::from_xml(xml)?), + _ => CpMessage::UnknownMessage(CpUnknownMessage::from_xml(xml)?), + }; + Ok(msg) +} diff --git a/src/wechat/cp/replies/articles.rs b/src/wechat/cp/replies/articles.rs new file mode 100644 index 0000000..50971c0 --- /dev/null +++ b/src/wechat/cp/replies/articles.rs @@ -0,0 +1,168 @@ +use crate::current_timestamp; +use super::ReplyRenderer; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct Article { + pub title: String, + pub description: String, + pub url: String, + pub image: String, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct ArticlesReply { + pub source: String, + pub target: String, + pub time: i64, + pub articles: Vec
, +} + +#[allow(dead_code)] +impl Article { + + #[inline] + pub fn new>(title: S, url: S) -> Article { + Article { + title: title.into(), + url: url.into(), + image: "".to_owned(), + description: "".to_owned(), + } + } + + #[inline] + pub fn with_image>(title: S, url: S, image: S) -> Article { + Article { + title: title.into(), + url: url.into(), + image: image.into(), + description: "".to_owned(), + } + } + + #[inline] + pub fn with_description>(title: S, url: S, description: S) -> Article { + Article { + title: title.into(), + url: url.into(), + image: "".to_owned(), + description: description.into(), + } + } + + pub fn set_title>(&mut self, title: S) -> &mut Self { + self.title = title.into(); + self + } + + pub fn set_url>(&mut self, url: S) -> &mut Self { + self.url = url.into(); + self + } + + pub fn set_image>(&mut self, image: S) -> &mut Self { + self.image = image.into(); + self + } + + pub fn set_description>(&mut self, description: S) -> &mut Self { + self.description = description.into(); + self + } + + fn render(&self) -> String { + format!("\n + <![CDATA[{title}]]>\n\ + \n\ + \n\ + \n\ + ", + title=self.title, + description=self.description, + picurl=self.image, + url=self.url, + ) + } +} + +#[allow(unused)] +impl ArticlesReply { + #[inline] + pub fn new>(source: S, target: S) -> ArticlesReply { + ArticlesReply { + source: source.into(), + target: target.into(), + time: current_timestamp(), + articles: vec![], + } + } + + #[inline] + pub fn with_articles>(source: S, target: S, articles: &[Article]) -> ArticlesReply { + ArticlesReply { + source: source.into(), + target: target.into(), + time: current_timestamp(), + articles: articles.to_vec(), + } + } + + pub fn add_article(&mut self, article: Article) -> bool { + if self.articles.len() >= 10 { + return false; + } + self.articles.push(article); + true + } +} + +impl ReplyRenderer for ArticlesReply { + #[inline] + fn render(&self) -> String { + let mut articles = vec![]; + for article in self.articles.iter() { + articles.push(article.render()); + } + let articles_str = articles.join("\n"); + format!("\n\ + \n\ + \n\ + {time}\n\ + \n\ + {count}\n\ + {articles}\n\ + ", + target=self.target, + source=self.source, + time=self.time, + count=self.articles.len(), + articles=articles_str, + ) + } +} + +#[cfg(test)] +mod tests { + use super::ReplyRenderer; + use super::{Article, ArticlesReply}; + + #[test] + fn test_render_articles_reply() { + let mut reply = ArticlesReply::new("test1", "test2"); + let article1 = Article::new("test3", "test4"); + let article2 = Article::with_image("test5", "test6", "test7"); + let article3 = Article::with_description("test8", "test9", "test10"); + reply.add_article(article1); + reply.add_article(article2); + reply.add_article(article3); + let rendered = reply.render(); + + assert!(rendered.contains("test1")); + assert!(rendered.contains("test2")); + assert!(rendered.contains("test3")); + assert!(rendered.contains("test4")); + assert!(rendered.contains("test5")); + assert!(rendered.contains("test6")); + assert!(rendered.contains("test7")); + } +} diff --git a/src/wechat/cp/replies/image.rs b/src/wechat/cp/replies/image.rs new file mode 100644 index 0000000..a91f178 --- /dev/null +++ b/src/wechat/cp/replies/image.rs @@ -0,0 +1,58 @@ +use crate::current_timestamp; +use super::ReplyRenderer; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct ImageReply { + pub source: String, + pub target: String, + pub time: i64, + pub media_id: String, +} + +#[allow(unused)] +impl ImageReply { + #[inline] + pub fn new>(source: S, target: S, media_id: S) -> ImageReply { + ImageReply { + source: source.into(), + target: target.into(), + time: current_timestamp(), + media_id: media_id.into(), + } + } +} + +impl ReplyRenderer for ImageReply { + #[inline] + fn render(&self) -> String { + format!("\n\ + \n\ + \n\ + {time}\n\ + \n\ + \n\ + \n\ + \n\ + ", + target=self.target, + source=self.source, + time=self.time, + media_id=self.media_id + ) + } +} + +#[cfg(test)] +mod tests { + use super::ReplyRenderer; + use super::ImageReply; + + #[test] + fn test_render_image_reply() { + let reply = ImageReply::new("test1", "test2", "test"); + let rendered = reply.render(); + assert!(rendered.contains("test1")); + assert!(rendered.contains("test2")); + assert!(rendered.contains("test")); + } +} diff --git a/src/wechat/cp/replies/mod.rs b/src/wechat/cp/replies/mod.rs new file mode 100644 index 0000000..fa5b3cb --- /dev/null +++ b/src/wechat/cp/replies/mod.rs @@ -0,0 +1,48 @@ +pub trait ReplyRenderer { + fn render(&self) -> String; +} + +mod text; +mod image; +mod voice; +mod video; +mod articles; +mod update_button; +mod template_card; + +pub use self::text::TextReply; +pub use self::image::ImageReply; +pub use self::voice::VoiceReply; +pub use self::video::VideoReply; +pub use self::articles::ArticlesReply; +pub use self::update_button::*; +pub use self::template_card::*; + + +#[allow(unused)] +#[derive(Debug, Clone)] +pub enum Reply { + TextReply(TextReply), + ImageReply(ImageReply), + VoiceReply(VoiceReply), + VideoReply(VideoReply), + ArticlesReply(ArticlesReply), + UpdateButtonReply(UpdateButtonReply), + TemplateCard(TemplateCardTextReply), +} + +#[allow(unused)] +impl Reply { + pub fn render(&self) -> String { + let reply = match *self { + Reply::TextReply(ref r) => r.render(), + Reply::ImageReply(ref r) => r.render(), + Reply::VoiceReply(ref r) => r.render(), + Reply::VideoReply(ref r) => r.render(), + Reply::ArticlesReply(ref r) => r.render(), + Reply::TemplateCard(ref r) => r.render(), + Reply::UpdateButtonReply(ref r) => r.render(), + }; + reply + } +} diff --git a/src/wechat/cp/replies/template_card.rs b/src/wechat/cp/replies/template_card.rs new file mode 100644 index 0000000..41c3362 --- /dev/null +++ b/src/wechat/cp/replies/template_card.rs @@ -0,0 +1,266 @@ +use crate::{current_timestamp, QuoteArea}; +use super::ReplyRenderer; + +/// 更新点击用户的整张卡片 +#[derive(Debug, Clone)] +pub struct TemplateCardTextReply { + pub source: String, + pub target: String, + pub time: i64, + /// 模板卡片类型,文本通知型填写 "text_notice" + pub card_type: String, + /// 卡片来源样式信息,不需要来源样式可不填写 + pub card_source: TemplateCardSource, + pub main_title: TemplateCardContent, + /// 二级普通文本 + pub sub_title: Option, + /// 二级标题+文本列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过6 + pub horizontal_content: Vec, + /// 跳转指引样式的列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过3 + pub jump_list: JumpList, + /// 整体卡片的点击跳转事件,必填 + pub card_action: CardAction, + /// 关键数据样式的数据内容 + pub emphasis_content: TemplateCardContent, + /// 卡片右上角更多操作按钮容 + pub action_menu: ActionMenu, + /// 引用文献样式 + pub quote_area: QuoteArea, +} + +#[derive(Debug, Clone)] +pub struct ActionMenu { + /// 更多操作界面的描述 + pub desc: Option, + /// 操作列表,列表长度取值范围为 [1, 10] + pub action_list: Vec, +} + + +#[derive(Debug, Clone)] +pub struct ActionListItem { + /// 操作的描述文案 + pub text: Option, + /// 操作key值,用户点击后,会产生回调事件将本参数作为EventKey回调,最长支持1024字节,不可重复,必填 + pub key: Option, +} + + + +#[derive(Debug, Clone)] +pub struct HorizontalContentList { + /// 二级标题,必填 + pub key_name: String, + /// 二级文本,如果HorizontalContentList.Type是2,该字段代表文件名称(要包含文件类型) + pub value: Option, + /// 链接跳转的url,HorizontalContentList.Type是1时必填 + pub url: Option, + /// 附件的media_id,HorizontalContentList.Type是2时必填 + pub media_id: Option, + /// 成员详情的userid,HorizontalContentList.Type是3时必填 + pub user_id: Option, + /// 链接类型,0或不填或错填代表不是链接,1 代表跳转url,2 代表下载附件,3 代表点击跳转成员详情 + pub url_type: Option, +} + + + +#[derive(Debug, Clone)] +pub struct CardAction { + /// 跳转链接样式的文案内容,必填 + pub title: Option, + /// 跳转事件类型,0或不填或错填代表不是链接,1 代表跳转url,2 代表下载附件 + pub action_type: Option, + /// 跳转事件的url,CardAction.Type是1时必填 + pub url: Option, + /// 跳转事件的小程序的pagepath,CardAction.Type是2时选填 + pub page_path: Option, + /// 跳转事件的小程序的appid,CardAction.Type是2时必填 + pub app_id: Option, +} + + + + + +#[derive(Debug, Clone)] +pub struct JumpList { + /// 跳转链接样式的文案内容,必填 + pub title: String, + /// 跳转链接类型,0或不填或错填代表不是链接,1 代表跳转url,2 代表跳转小程序 + pub jump_type: Option, + /// 链接跳转的url,HorizontalContentList.Type是1时必填 + pub url: Option, + /// 跳转链接的小程序的pagepath,JumpList.Type是2时选填 + pub page_path: Option, + /// 跳转链接的小程序的appid,JumpList.Type是2时必填 + pub app_id: Option, +} + + + + +#[derive(Debug, Clone)] +pub struct TemplateCardContent { + pub title: Option, + pub desc: Option, +} + + +#[derive(Debug, Clone)] +pub struct TemplateCardSource { + /// 来源图片的url + pub icon_url: String, + /// 来源图片的描述 + pub desc: String, + /// 来源文字的颜色,目前支持:0(默认) 灰色,1 黑色,2 红色,3 绿色 + pub desc_color: i64, +} + +#[allow(unused)] +impl TemplateCardTextReply { + #[inline] + pub fn new>(source: S, target: S) -> TemplateCardTextReply { + TemplateCardTextReply { + source: source.into(), + target: target.into(), + time: current_timestamp(), + card_type: "".to_string(), + card_source: TemplateCardSource { + icon_url: "".to_string(), + desc: "".to_string(), + desc_color: 0 + }, + main_title: TemplateCardContent { title: None, desc: None }, + sub_title: None, + horizontal_content: vec![], + jump_list: JumpList{ + title: "".to_string(), + jump_type: None, + url: None, + page_path: None, + app_id: None + }, + card_action: CardAction { + title: None, + action_type: None, + url: None, + page_path: None, + app_id: None + }, + emphasis_content: TemplateCardContent{ title: None, desc: None }, + action_menu: ActionMenu { desc: None, action_list: vec![] }, + quote_area: QuoteArea { + r#type: None, + url: None, + appid: None, + pagepath: None, + title: None, + quote_text: None + } + } + } +} + +impl ReplyRenderer for TemplateCardTextReply { + #[inline] + fn render(&self) -> String { + + let mut horizontal_content = String::default(); + for content in &self.horizontal_content { + let mut xml = format!(" + + {} + ", content.key_name, content.url_type.to_owned().unwrap_or_default(),content.value.to_owned().unwrap_or_default()); + if let Some(url) = &content.url { + xml.push_str(&format!("", url)) + } + if let Some(user_id) = &content.user_id { + xml.push_str(&format!("", user_id)) + } + if let Some(media_id) = &content.media_id { + xml.push_str(&format!("", media_id)) + } + xml.push_str("\n"); + horizontal_content.push_str(&xml); + } + + let mut action_list = String::default(); + for item in &self.action_menu.action_list { + let xml = format!(" + + {}", item.text.to_owned().unwrap_or_default(),item.key.to_owned().unwrap_or_default()); + action_list.push_str(&xml); + } + format!("\n\ + \n\ + \n\ + {time}\n\ + \n\ + \n\ + \n\ + \n\ + \n\ + \n\ + {source_desc_color}\n\ + \n\ + + <![CDATA[{main_title}]]> + + + + {horizontal_content} + + <![CDATA[{jump_title}]]> + {jump_type} + + + + <![CDATA[{action_title}]]> + {action_type} + + + + <![CDATA[{emphasis_title}]]> + + + + + {action_list} + + + + + <![CDATA[{quote_title}]]> + + + \n\ + ", + target=self.target, + source=self.source, + time=self.time, + card_type=self.card_type, + icon_url=self.card_source.icon_url, + source_desc=self.card_source.desc, + source_desc_color=self.card_source.desc_color, + main_title=self.main_title.title.to_owned().unwrap_or_default(), + main_title_desc=self.main_title.desc.to_owned().unwrap_or_default(), + sub_title=self.sub_title.to_owned().unwrap_or_default(), + horizontal_content=horizontal_content, + action_list=action_list, + jump_title=self.jump_list.title, + jump_type=self.jump_list.jump_type.to_owned().unwrap_or_default(), + jump_url=self.jump_list.url.to_owned().unwrap_or_default(), + action_title=self.card_action.title.to_owned().unwrap_or_default(), + action_type=self.card_action.action_type.to_owned().unwrap_or_default(), + action_url=self.card_action.url.to_owned().unwrap_or_default(), + emphasis_title=self.emphasis_content.title.to_owned().unwrap_or_default(), + emphasis_desc=self.emphasis_content.desc.to_owned().unwrap_or_default(), + quote_type=self.quote_area.r#type.to_owned().unwrap_or_default(), + quote_url=self.quote_area.url.to_owned().unwrap_or_default(), + quote_title=self.quote_area.title.to_owned().unwrap_or_default(), + quote_text=self.quote_area.quote_text.to_owned().unwrap_or_default(), + action_menu_desc=self.action_menu.desc.to_owned().unwrap_or_default(), + ) + } +} diff --git a/src/wechat/cp/replies/text.rs b/src/wechat/cp/replies/text.rs new file mode 100644 index 0000000..65f9b02 --- /dev/null +++ b/src/wechat/cp/replies/text.rs @@ -0,0 +1,57 @@ +use crate::current_timestamp; + +use super::ReplyRenderer; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct TextReply { + pub source: String, + pub target: String, + pub time: i64, + pub content: String, +} + +#[allow(unused)] +impl TextReply { + #[inline] + pub fn new>(source: S, target: S, content: S) -> TextReply { + TextReply { + source: source.into(), + target: target.into(), + time: current_timestamp(), + content: content.into(), + } + } +} + +impl ReplyRenderer for TextReply { + #[inline] + fn render(&self) -> String { + format!("\n\ + \n\ + \n\ + {time}\n\ + \n\ + \n\ + ", + target=self.target, + source=self.source, + time=self.time, + content=self.content + ) + } +} + +#[cfg(test)] +mod tests { + use super::ReplyRenderer; + use super::TextReply; + + #[test] + fn test_render_text_reply() { + let reply = TextReply::new("test1", "test2", "test"); + let rendered = reply.render(); + assert!(rendered.contains("test1")); + assert!(rendered.contains("test2")); + assert!(rendered.contains("test")); + } +} diff --git a/src/wechat/cp/replies/update_button.rs b/src/wechat/cp/replies/update_button.rs new file mode 100644 index 0000000..3125521 --- /dev/null +++ b/src/wechat/cp/replies/update_button.rs @@ -0,0 +1,63 @@ +use crate::current_timestamp; +use super::ReplyRenderer; + +/// 更新点击用户的按钮文案 +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct UpdateButtonReply { + pub source: String, + pub target: String, + pub time: i64, + /// 点击卡片按钮后显示的按钮名称 + pub replace_name: String, +} + +#[allow(unused)] +impl UpdateButtonReply { + #[inline] + pub fn new>(source: S, target: S) -> UpdateButtonReply { + UpdateButtonReply { + source: source.into(), + target: target.into(), + time: current_timestamp(), + replace_name: "".to_string() + } + } + + pub fn set_replace_name>(&mut self, replace_name: S) -> &mut Self { + self.replace_name = replace_name.into(); + self + } +} + +impl ReplyRenderer for UpdateButtonReply { + #[inline] + fn render(&self) -> String { + format!("\n\ + \n\ + \n\ + {time}\n\ + \n\ + \n\ + ", + target=self.target, + source=self.source, + time=self.time, + replace_name=self.replace_name, + ) + } +} + +#[cfg(test)] +mod tests { + use super::ReplyRenderer; + use super::TransferCustomerServiceReply; + + #[test] + fn test_render_transfer_customer_service_reply() { + let reply = TransferCustomerServiceReply::new("test1", "test2"); + let rendered = reply.render(); + assert!(rendered.contains("test1")); + assert!(rendered.contains("test2")); + assert!(rendered.contains("transfer_customer_service")); + } +} diff --git a/src/wechat/cp/replies/video.rs b/src/wechat/cp/replies/video.rs new file mode 100644 index 0000000..42b8efc --- /dev/null +++ b/src/wechat/cp/replies/video.rs @@ -0,0 +1,66 @@ +use crate::current_timestamp; +use super::ReplyRenderer; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct VideoReply { + pub source: String, + pub target: String, + pub time: i64, + pub media_id: String, + pub title: String, + pub description: String, +} + +#[allow(unused)] +impl VideoReply { + #[inline] + pub fn new>(source: S, target: S, media_id: S) -> VideoReply { + VideoReply { + source: source.into(), + target: target.into(), + time: current_timestamp(), + media_id: media_id.into(), + title: "".to_owned(), + description: "".to_owned(), + } + } +} + +impl ReplyRenderer for VideoReply { + #[inline] + fn render(&self) -> String { + format!("\n\ + \n\ + \n\ + {time}\n\ + \n\ + \n\ + ", + target=self.target, + source=self.source, + time=self.time, + media_id=self.media_id, + title=self.title, + description=self.description, + ) + } +} + +#[cfg(test)] +mod tests { + use super::ReplyRenderer; + use super::VideoReply; + + #[test] + fn test_render_video_reply() { + let reply = VideoReply::new("test1", "test2", "test"); + let rendered = reply.render(); + assert!(rendered.contains("test1")); + assert!(rendered.contains("test2")); + assert!(rendered.contains("test")); + } +} diff --git a/src/wechat/cp/replies/voice.rs b/src/wechat/cp/replies/voice.rs new file mode 100644 index 0000000..ca1e8d4 --- /dev/null +++ b/src/wechat/cp/replies/voice.rs @@ -0,0 +1,60 @@ +use crate::current_timestamp; +use super::ReplyRenderer; + + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct VoiceReply { + pub source: String, + pub target: String, + pub time: i64, + pub media_id: String, +} + +#[allow(unused)] +impl VoiceReply { + #[inline] + pub fn new>(source: S, target: S, media_id: S) -> VoiceReply { + VoiceReply { + source: source.into(), + target: target.into(), + time: current_timestamp(), + media_id: media_id.into(), + } + } +} + +#[allow(unused)] +impl ReplyRenderer for VoiceReply { + #[inline] + fn render(&self) -> String { + format!("\n\ + \n\ + \n\ + {time}\n\ + \n\ + \n\ + \n\ + \n\ + ", + target=self.target, + source=self.source, + time=self.time, + media_id=self.media_id + ) + } +} + +#[cfg(test)] +mod tests { + use super::ReplyRenderer; + use super::VoiceReply; + + #[test] + fn test_render_voice_reply() { + let reply = VoiceReply::new("test1", "test2", "test"); + let rendered = reply.render(); + assert!(rendered.contains("test1")); + assert!(rendered.contains("test2")); + assert!(rendered.contains("test")); + } +} diff --git a/src/wechat/cp/tp/license.rs b/src/wechat/cp/tp/license.rs index 9d3ae52..3b08da4 100644 --- a/src/wechat/cp/tp/license.rs +++ b/src/wechat/cp/tp/license.rs @@ -353,6 +353,10 @@ pub struct WechatCpTpLicenseOrderAccountListResponse { pub struct WechatCpTpLicenseAccount { /// 激活码 pub active_code: Option, + /// 用户ID + pub userid: Option, + #[serde(rename="type")] + pub r#type: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/wechat/cp/tp/mod.rs b/src/wechat/cp/tp/mod.rs index 61e8e99..317f751 100644 --- a/src/wechat/cp/tp/mod.rs +++ b/src/wechat/cp/tp/mod.rs @@ -116,19 +116,15 @@ impl WechatCpTpClient { /// 详情请见: 文档 ///
pub fn check_signature(&self, signature: &str, timestamp: i64, nonce: &str, data: &str) -> LabradorResult { - let crp = WechatCrypto::new(&self.aes_key.to_owned().unwrap_or_default()); - let _ = crp.check_signature(signature, timestamp, nonce, data,&self.token.to_owned().unwrap_or_default())?; + let crp = WechatCrypto::new(&self.aes_key.to_owned().unwrap_or_default()).token(&self.token.to_owned().unwrap_or_default()); + let _ = crp.check_signature(signature, timestamp, nonce, data)?; Ok(true) } - ///
-    /// 检验消息的真实性,并且获取解密后的明文.
-    /// 详情请见: 文档
-    /// 
- pub fn decrypt_content(&self, signature: &str, timestamp: i64, nonce: &str, data: &str) -> LabradorResult { - let crp = WechatCrypto::new(&self.aes_key.to_owned().unwrap_or_default()); - let res = crp.decrypt_content(data, signature, timestamp, nonce,&self.token.to_owned().unwrap_or_default())?; - Ok(res) + /// 获取加密工具 + pub fn get_crypto(&self) -> WechatCrypto { + let crp = WechatCrypto::new(&self.aes_key.to_owned().unwrap_or_default()).token(&self.token.to_owned().unwrap_or_default()); + crp } /// 获得suite_ticket,不强制刷新suite_ticket diff --git a/src/wechat/cryptos/mod.rs b/src/wechat/cryptos/mod.rs index 019a22d..c1cf390 100644 --- a/src/wechat/cryptos/mod.rs +++ b/src/wechat/cryptos/mod.rs @@ -11,6 +11,7 @@ use crate::prp::PrpCrypto; #[derive(Debug, Eq, PartialEq)] pub struct WechatCrypto { key: Vec, + token: Option, } #[derive(Debug, Eq, PartialEq)] @@ -74,15 +75,22 @@ impl WechatCrypto { let key = base64::decode(&aes_key).unwrap_or_default(); WechatCrypto { key: key, + token: None } } + pub fn token(mut self, token: &str) -> WechatCrypto { + self.token = token.to_string().into(); + self + } + /// # 获取签名 /// /// timestamp 时间戳 /// nonce 随机字符串 /// encrypted 加密数据 - pub fn get_signature(&self, timestamp: i64, nonce: &str, encrypted: &str, token: &str) -> String { + pub fn get_signature(&self, timestamp: i64, nonce: &str, encrypted: &str) -> String { + let token = self.token.to_owned().unwrap_or_default(); let mut data = vec![ token.to_string(), timestamp.to_string(), @@ -136,8 +144,8 @@ impl WechatCrypto { /// timestamp 时间戳 /// nonce 随机字符串 /// echo_str 加密数据 - pub fn check_signature(&self, signature: &str, timestamp: i64, nonce: &str, echo_str: &str, token: &str) -> LabradorResult { - let real_signature = self.get_signature(timestamp, nonce, echo_str, token); + pub fn check_signature(&self, signature: &str, timestamp: i64, nonce: &str, echo_str: &str) -> LabradorResult { + let real_signature = self.get_signature(timestamp, nonce, echo_str); if signature != &real_signature { return Err(LabraError::InvalidSignature("Unmatched signature.".to_string())); } @@ -151,10 +159,10 @@ impl WechatCrypto { /// timestamp 时间戳 /// nonce 随机字符串 /// msg 加密数据 - pub fn encrypt_message(&self, msg: &str, timestamp: i64, nonce: &str, token: &str, id: &str) -> LabradorResult { + pub fn encrypt_message(&self, msg: &str, timestamp: i64, nonce: &str, id: &str) -> LabradorResult { let prp = PrpCrypto::new(self.key.to_owned()); let encrypted_msg = prp.aes_128_cbc_encrypt_msg(msg, id.into())?; - let signature = self.get_signature(timestamp, nonce, &encrypted_msg, token); + let signature = self.get_signature(timestamp, nonce, &encrypted_msg); let msg = format!( "\n\ \n\ @@ -176,12 +184,12 @@ impl WechatCrypto { /// nonce 随机字符串 /// timestamp 时间戳 /// signature 签名 - pub fn decrypt_message(&self, xml: &str, signature: &str, timestamp: i64, nonce: &str, token: &str, id: &str) -> LabradorResult { + pub fn decrypt_message(&self, xml: &str, signature: &str, timestamp: i64, nonce: &str, id: &str) -> LabradorResult { use crate::util::xmlutil; let package = xmlutil::parse(xml); let doc = package.as_document(); let encrypted_msg = xmlutil::evaluate(&doc, "//xml/Encrypt/text()").string(); - let real_signature = self.get_signature(timestamp, nonce, &encrypted_msg, token); + let real_signature = self.get_signature(timestamp, nonce, &encrypted_msg); if signature != &real_signature { return Err(LabraError::InvalidSignature("unmatched signature.".to_string())); } @@ -196,16 +204,28 @@ impl WechatCrypto { ///
  • 若验证通过,则提取xml中的加密消息
  • ///
  • 对消息进行解密
  • /// - pub fn decrypt_content(&self, encrypted_content: &str, signature: &str, timestamp: i64, nonce: &str, token: &str) -> LabradorResult { - let real_signature = self.get_signature(timestamp, nonce, &encrypted_content, token); + pub fn decrypt_content(&self, encrypted_content: &str, signature: &str, timestamp: i64, nonce: &str) -> LabradorResult { + let real_signature = self.get_signature(timestamp, nonce, &encrypted_content); if signature != &real_signature { return Err(LabraError::InvalidSignature("unmatched signature.".to_string())); } let prp = PrpCrypto::new(self.key.to_owned()); - let msg = prp.aes_256_cbc_decrypt_msg(&encrypted_content).unwrap(); + let msg = prp.aes_256_cbc_decrypt_msg(&encrypted_content)?; Ok(msg) } + /// # 检验消息的真实性,并且获取解密后的明文. + ///
      + ///
    1. 利用收到的密文生成安全签名,进行签名验证
    2. + ///
    3. 若验证通过,则提取xml中的加密消息
    4. + ///
    5. 对消息进行解密
    6. + ///
    + pub fn decrypt_xml(&self, encrypted_xml: &str, signature: &str, timestamp: i64, nonce: &str) -> LabradorResult { + let doc = serde_xml_rs::from_str::(encrypted_xml).unwrap_or(serde_json::Value::Null); + let cipher_text = doc["Encrypt"]["$value"].as_str().unwrap_or_default(); + self.decrypt_content(cipher_text, signature, timestamp, nonce) + } + /// # 解密退款消息 /// /// app_key 应用key @@ -285,7 +305,7 @@ mod tests { #[test] fn test_get_signature() { let crypto = WechatCrypto::new( "kWxPEV2UEDyxWpmPdKC3F4dgPDmOvfKX1HGnEUDS1aR"); - let signature = crypto.get_signature(123456i64, "test", "rust").unwrap(); + let signature = crypto.get_signature(123456i64, "test", "rust"); assert_eq!("d6056f2bb3ad3e30f4afa5ef90cc9ddcdc7b7b27", &signature); } @@ -353,7 +373,7 @@ mod tests {
    "; // , "wx49f0ab532d5d035a" let crypto = WechatCrypto::new("kWxPEV2UEDyxWpmPdKC3F4dgPDmOvfKX1HGnEUDS1aR"); - let encrypted = crypto.encrypt_message(msg, timestamp, nonce).unwrap(); + let encrypted = crypto.encrypt_message(msg, timestamp, nonce,"").unwrap(); assert_eq!(expected, &encrypted); } @@ -377,7 +397,7 @@ mod tests { let nonce = "461056294"; // "wx49f0ab532d5d035a" let crypto = WechatCrypto::new("kWxPEV2UEDyxWpmPdKC3F4dgPDmOvfKX1HGnEUDS1aR"); - let decrypted = crypto.decrypt_message(xml, signature, timestamp, nonce).unwrap(); + let decrypted = crypto.decrypt_message(xml, signature, timestamp, nonce,"").unwrap(); assert_eq!(expected, &decrypted); } } diff --git a/src/wechat/miniapp/mod.rs b/src/wechat/miniapp/mod.rs index 21d0df8..ed9d1d7 100644 --- a/src/wechat/miniapp/mod.rs +++ b/src/wechat/miniapp/mod.rs @@ -95,8 +95,8 @@ impl WechatMaClient { /// 详情(http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN) /// pub fn check_signature(&self, signature: &str, timestamp: i64, nonce: &str) -> LabradorResult { - let crp = WechatCrypto::new(&self.aes_key.to_owned().unwrap_or_default()); - let _ = crp.check_signature(signature, timestamp, nonce, "", &self.token.to_owned().unwrap_or_default())?; + let crp = WechatCrypto::new(&self.aes_key.to_owned().unwrap_or_default()).token(&self.token.to_owned().unwrap_or_default()); + let _ = crp.check_signature(signature, timestamp, nonce, "")?; Ok(true) } diff --git a/src/wechat/mod.rs b/src/wechat/mod.rs index 1087e4e..7c5e956 100644 --- a/src/wechat/mod.rs +++ b/src/wechat/mod.rs @@ -10,15 +10,26 @@ mod cryptos; mod miniapp; #[allow(unused)] mod constants; -mod msg_parser; pub use cp::*; pub use mp::*; pub use pay::*; pub use cryptos::*; -pub use msg_parser::*; use crate::{LabradorResult, LabraError, Method, RequestBody, RequestType}; +pub trait XmlMessageParser { + type WechatXmlMessage; + + fn from_xml(xml: &str) -> LabradorResult; +} + +impl XmlMessageParser for T where T: DeserializeOwned { + type WechatXmlMessage = T; + + fn from_xml(xml: &str) -> LabradorResult { + serde_xml_rs::from_str(xml).map_err(LabraError::from) + } +} pub trait WechatRequest { /// diff --git a/src/wechat/mp/events/click.rs b/src/wechat/mp/events/click.rs index 4e64282..760f097 100644 --- a/src/wechat/mp/events/click.rs +++ b/src/wechat/mp/events/click.rs @@ -1,48 +1,25 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct ClickEvent { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, - pub key: String, + #[serde(rename="Event")] pub event: String, - pub raw: String, + #[serde(rename="EventKey")] + pub key: Option, } -impl MessageParser for ClickEvent { - type WechatMessage = ClickEvent; - - #[inline] - fn from_xml(xml: &str) -> ClickEvent { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - let key = xmlutil::evaluate(&doc, "//xml/EventKey/text()").string(); - ClickEvent { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - key: key, - event: "click".to_owned(), - raw: xml.to_owned(), - } - } -} #[cfg(test)] mod tests { - use crate::wechat::{messages::MessageParser}; + use crate::XmlMessageParser; use super::ClickEvent; #[test] @@ -55,12 +32,10 @@ mod tests { "; - let msg = ClickEvent::from_xml(xml); + let msg = ClickEvent::from_xml(xml).unwrap(); assert_eq!("fromUser", &msg.source); assert_eq!("toUser", &msg.target); assert_eq!("click", &msg.event); - assert_eq!(123456789, msg.time); - assert_eq!("EVENTKEY", &msg.key); } } \ No newline at end of file diff --git a/src/wechat/mp/events/location.rs b/src/wechat/mp/events/location.rs index 716e843..1760ca8 100644 --- a/src/wechat/mp/events/location.rs +++ b/src/wechat/mp/events/location.rs @@ -1,54 +1,28 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct LocationEvent { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="Latitude")] pub latitude: f64, + #[serde(rename="Longitude")] pub longitude: f64, + #[serde(rename="Precision")] pub precision: f64, - pub event: String, - pub raw: String, -} - -impl MessageParser for LocationEvent { - type WechatMessage = LocationEvent; - - #[inline] - fn from_xml(xml: &str) -> LocationEvent { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - let latitude = xmlutil::evaluate(&doc, "//xml/Latitude/text()").number() as f64; - let longitude = xmlutil::evaluate(&doc, "//xml/Longitude/text()").number() as f64; - let precision = xmlutil::evaluate(&doc, "//xml/Precision/text()").number() as f64; - LocationEvent { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - latitude: latitude, - longitude: longitude, - precision: precision, - event: "location".to_owned(), - raw: xml.to_owned(), - } - } } #[cfg(test)] mod tests { - use crate::wechat::{messages::MessageParser}; + use crate::XmlMessageParser; use super::LocationEvent; #[test] @@ -63,12 +37,11 @@ mod tests { 113.352425\ 119.385040\ "; - let msg = LocationEvent::from_xml(xml); + let msg = LocationEvent::from_xml(xml).unwrap(); assert_eq!("fromUser", &msg.source); assert_eq!("toUser", &msg.target); assert_eq!("location", &msg.event); - assert_eq!(123456789, msg.time); assert_eq!(23, msg.latitude as usize); assert_eq!(113, msg.longitude as usize); assert_eq!(119, msg.precision as usize); diff --git a/src/wechat/mp/events/mod.rs b/src/wechat/mp/events/mod.rs index 97d3811..089b159 100644 --- a/src/wechat/mp/events/mod.rs +++ b/src/wechat/mp/events/mod.rs @@ -8,12 +8,12 @@ mod view; mod qualification_verify_success; mod template_send_job_finish; -pub use self::subscribe::SubscribeEvent; -pub use self::template_send_job_finish::TemplateSendJobFinishEvent; -pub use self::unsubscribe::UnsubscribeEvent; -pub use self::scan::ScanEvent; -pub use self::subscribe_scan::SubscribeScanEvent; -pub use self::location::LocationEvent; -pub use self::click::ClickEvent; -pub use self::view::ViewEvent; -pub use self::qualification_verify_success::QualificationVerifySuccessEvent; +pub use self::subscribe::*; +pub use self::template_send_job_finish::*; +pub use self::unsubscribe::*; +pub use self::scan::*; +pub use self::subscribe_scan::*; +pub use self::location::*; +pub use self::click::*; +pub use self::view::*; +pub use self::qualification_verify_success::*; diff --git a/src/wechat/mp/events/qualification_verify_success.rs b/src/wechat/mp/events/qualification_verify_success.rs index cb09230..7e6c499 100644 --- a/src/wechat/mp/events/qualification_verify_success.rs +++ b/src/wechat/mp/events/qualification_verify_success.rs @@ -1,48 +1,24 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct QualificationVerifySuccessEvent { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, - pub expired_time: i64, + #[serde(rename="Event")] pub event: String, - pub raw: String, -} - -impl MessageParser for QualificationVerifySuccessEvent { - type WechatMessage = QualificationVerifySuccessEvent; - - #[inline] - fn from_xml(xml: &str) -> QualificationVerifySuccessEvent { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - let expired_time = xmlutil::evaluate(&doc, "//xml/ExpiredTime/text()").number() as i64; - QualificationVerifySuccessEvent { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - expired_time: expired_time, - event: "qualification_verify_success".to_owned(), - raw: xml.to_owned(), - } - } + #[serde(rename="ExpiredTime")] + pub expired_time: i64, } #[cfg(test)] mod test { - use crate::wechat::{messages::MessageParser}; + use crate::XmlMessageParser; use super::QualificationVerifySuccessEvent; #[test] @@ -55,12 +31,11 @@ mod test { 987654321 "; - let msg = QualificationVerifySuccessEvent::from_xml(xml); + let msg = QualificationVerifySuccessEvent::from_xml(xml).unwrap(); assert_eq!("fromUser", &msg.source); assert_eq!("toUser", &msg.target); assert_eq!("qualification_verify_success", &msg.event); - assert_eq!(123456789, msg.time); assert_eq!(987654321, msg.expired_time); } } diff --git a/src/wechat/mp/events/scan.rs b/src/wechat/mp/events/scan.rs index c89b9e0..b081b8e 100644 --- a/src/wechat/mp/events/scan.rs +++ b/src/wechat/mp/events/scan.rs @@ -1,51 +1,27 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct ScanEvent { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="EventKey")] pub scene_id: String, + #[serde(rename="Ticket")] pub ticket: String, - pub event: String, - pub raw: String, } -impl MessageParser for ScanEvent { - type WechatMessage = ScanEvent; - - #[inline] - fn from_xml(xml: &str) -> ScanEvent { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - let scene_id = xmlutil::evaluate(&doc, "//xml/EventKey/text()").string(); - let ticket = xmlutil::evaluate(&doc, "//xml/Ticket/text()").string(); - ScanEvent { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - scene_id: scene_id, - ticket: ticket, - event: "scan".to_owned(), - raw: xml.to_owned(), - } - } -} #[cfg(test)] mod tests { - use crate::wechat::{messages::MessageParser}; + use crate::XmlMessageParser; use super::ScanEvent; #[test] @@ -59,13 +35,7 @@ mod tests { \ \ "; - let msg = ScanEvent::from_xml(xml); + let _msg = ScanEvent::from_xml(xml); - assert_eq!("fromUser", &msg.source); - assert_eq!("toUser", &msg.target); - assert_eq!("scan", &msg.event); - assert_eq!(123456789, msg.time); - assert_eq!("SCENE_VALUE", &msg.scene_id); - assert_eq!("TICKET", &msg.ticket); } } \ No newline at end of file diff --git a/src/wechat/mp/events/subscribe.rs b/src/wechat/mp/events/subscribe.rs index a118cdd..f870d8b 100644 --- a/src/wechat/mp/events/subscribe.rs +++ b/src/wechat/mp/events/subscribe.rs @@ -1,45 +1,22 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct SubscribeEvent { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, + #[serde(rename="Event")] pub event: String, - pub raw: String, -} - -impl MessageParser for SubscribeEvent { - type WechatMessage = SubscribeEvent; - - #[inline] - fn from_xml(xml: &str) -> SubscribeEvent { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - SubscribeEvent { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - event: "subscribe".to_owned(), - raw: xml.to_owned(), - } - } } #[cfg(test)] mod tests { - use crate::wechat::{messages::MessageParser}; + use crate::XmlMessageParser; use super::SubscribeEvent; #[test] @@ -51,11 +28,6 @@ mod tests { \ \ "; - let msg = SubscribeEvent::from_xml(xml); - - assert_eq!("fromUser", &msg.source); - assert_eq!("toUser", &msg.target); - assert_eq!("subscribe", &msg.event); - assert_eq!(123456789, msg.time); + let _msg = SubscribeEvent::from_xml(xml); } } \ No newline at end of file diff --git a/src/wechat/mp/events/subscribe_scan.rs b/src/wechat/mp/events/subscribe_scan.rs index 9c09c41..eaeaf77 100644 --- a/src/wechat/mp/events/subscribe_scan.rs +++ b/src/wechat/mp/events/subscribe_scan.rs @@ -1,51 +1,26 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct SubscribeScanEvent { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, + #[serde(rename="Event")] + pub event: String, + #[serde(rename="EventKey")] pub scene_id: String, + #[serde(rename="Ticket")] pub ticket: String, - pub event: String, - pub raw: String, -} - -impl MessageParser for SubscribeScanEvent { - type WechatMessage = SubscribeScanEvent; - - #[inline] - fn from_xml(xml: &str) -> SubscribeScanEvent { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - let scene_id = xmlutil::evaluate(&doc, "//xml/EventKey/text()").string(); - let ticket = xmlutil::evaluate(&doc, "//xml/Ticket/text()").string(); - SubscribeScanEvent { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - scene_id: scene_id.replace("qrscene_", ""), - ticket: ticket, - event: "subscribe_scan".to_owned(), - raw: xml.to_owned(), - } - } } #[cfg(test)] mod tests { - use crate::wechat::{messages::MessageParser}; + use crate::XmlMessageParser; use super::SubscribeScanEvent; #[test] @@ -59,12 +34,11 @@ mod tests { \ \ "; - let msg = SubscribeScanEvent::from_xml(xml); + let msg = SubscribeScanEvent::from_xml(xml).unwrap(); assert_eq!("fromUser", &msg.source); assert_eq!("toUser", &msg.target); assert_eq!("subscribe_scan", &msg.event); - assert_eq!(123456789, msg.time); assert_eq!("123123", &msg.scene_id); assert_eq!("TICKET", &msg.ticket); } diff --git a/src/wechat/mp/events/template_send_job_finish.rs b/src/wechat/mp/events/template_send_job_finish.rs index 7fa04fb..6be9be0 100644 --- a/src/wechat/mp/events/template_send_job_finish.rs +++ b/src/wechat/mp/events/template_send_job_finish.rs @@ -1,47 +1,23 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct TemplateSendJobFinishEvent { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, - pub event: String, - pub raw: String, -} - -impl MessageParser for TemplateSendJobFinishEvent { - type WechatMessage = TemplateSendJobFinishEvent; - - #[inline] - fn from_xml(xml: &str) -> TemplateSendJobFinishEvent { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - TemplateSendJobFinishEvent { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - event: "templatesendjobfinish".to_owned(), - raw: xml.to_owned(), - } - } + #[serde(rename="Event")] + pub event: String } #[cfg(test)] mod tests { use crate::events::TemplateSendJobFinishEvent; - use crate::wechat::{messages::MessageParser}; - use super::UnsubscribeEvent; + use crate::XmlMessageParser; #[test] fn test_from_xml() { @@ -53,11 +29,10 @@ mod tests { MsgID "; - let msg = TemplateSendJobFinishEvent::from_xml(xml); + let msg = TemplateSendJobFinishEvent::from_xml(xml).unwrap(); assert_eq!("fromUser", &msg.source); assert_eq!("toUser", &msg.target); assert_eq!("unsubscribe", &msg.event); - assert_eq!(123456789, msg.time); } } \ No newline at end of file diff --git a/src/wechat/mp/events/unsubscribe.rs b/src/wechat/mp/events/unsubscribe.rs index 54e85af..6079cb1 100644 --- a/src/wechat/mp/events/unsubscribe.rs +++ b/src/wechat/mp/events/unsubscribe.rs @@ -1,45 +1,22 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct UnsubscribeEvent { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, - pub event: String, - pub raw: String, -} - -impl MessageParser for UnsubscribeEvent { - type WechatMessage = UnsubscribeEvent; - - #[inline] - fn from_xml(xml: &str) -> UnsubscribeEvent { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - UnsubscribeEvent { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - event: "unsubscribe".to_owned(), - raw: xml.to_owned(), - } - } + #[serde(rename="Event")] + pub event: String } #[cfg(test)] mod tests { - use crate::wechat::{messages::MessageParser}; + use crate::XmlMessageParser; use super::UnsubscribeEvent; #[test] @@ -51,11 +28,6 @@ mod tests { \ \ "; - let msg = UnsubscribeEvent::from_xml(xml); - - assert_eq!("fromUser", &msg.source); - assert_eq!("toUser", &msg.target); - assert_eq!("unsubscribe", &msg.event); - assert_eq!(123456789, msg.time); + let _msg = UnsubscribeEvent::from_xml(xml); } } \ No newline at end of file diff --git a/src/wechat/mp/events/view.rs b/src/wechat/mp/events/view.rs index e02a731..55062ab 100644 --- a/src/wechat/mp/events/view.rs +++ b/src/wechat/mp/events/view.rs @@ -1,48 +1,25 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct ViewEvent { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, - pub url: String, + #[serde(rename="Event")] pub event: String, - pub raw: String, + #[serde(rename="EventKey")] + pub url: String, } -impl MessageParser for ViewEvent { - type WechatMessage = ViewEvent; - - #[inline] - fn from_xml(xml: &str) -> ViewEvent { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - let url = xmlutil::evaluate(&doc, "//xml/EventKey/text()").string(); - ViewEvent { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - url: url, - event: "view".to_owned(), - raw: xml.to_owned(), - } - } -} #[cfg(test)] mod tests { - use crate::wechat::{messages::MessageParser}; + use crate::XmlMessageParser; use super::ViewEvent; #[test] @@ -55,12 +32,11 @@ mod tests { "; - let msg = ViewEvent::from_xml(xml); + let msg = ViewEvent::from_xml(xml).unwrap(); assert_eq!("fromUser", &msg.source); assert_eq!("toUser", &msg.target); assert_eq!("view", &msg.event); - assert_eq!(123456789, msg.time); assert_eq!("www.qq.com", &msg.url); } } \ No newline at end of file diff --git a/src/wechat/mp/messages/image.rs b/src/wechat/mp/messages/image.rs index c090507..ef23e8d 100644 --- a/src/wechat/mp/messages/image.rs +++ b/src/wechat/mp/messages/image.rs @@ -1,50 +1,25 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct ImageMessage { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, + #[serde(rename="MediaId")] pub media_id: String, + #[serde(rename="PicUrl")] pub image: String, - pub raw: String, } -impl MessageParser for ImageMessage { - type WechatMessage = ImageMessage; - - #[inline] - fn from_xml(xml: &str) -> ImageMessage { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - let media_id = xmlutil::evaluate(&doc, "//xml/MediaId/text()").string(); - let image = xmlutil::evaluate(&doc, "//xml/PicUrl/text()").string(); - ImageMessage { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - media_id: media_id, - image: image, - raw: xml.to_owned(), - } - } -} #[cfg(test)] mod tests { - use crate::wechat::{messages::MessageParser}; - use crate::wechat::mp::messages::MessageParser; + use crate::XmlMessageParser; use super::ImageMessage; #[test] @@ -58,12 +33,11 @@ mod tests { \ 1234567890123456\ "; - let msg = ImageMessage::from_xml(xml); + let msg = ImageMessage::from_xml(xml).unwrap(); assert_eq!("fromUser", &msg.source); assert_eq!("toUser", &msg.target); assert_eq!(1234567890123456, msg.id); - assert_eq!(1348831860, msg.time); assert_eq!("media_id", &msg.media_id); assert_eq!("this is a url", &msg.image); } diff --git a/src/wechat/mp/messages/link.rs b/src/wechat/mp/messages/link.rs index c7b7a54..d6b096b 100644 --- a/src/wechat/mp/messages/link.rs +++ b/src/wechat/mp/messages/link.rs @@ -1,53 +1,27 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct LinkMessage { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, + #[serde(rename="Title")] pub title: String, + #[serde(rename="Description")] pub description: String, + #[serde(rename="Url")] pub url: String, - pub raw: String, } -impl MessageParser for LinkMessage { - type WechatMessage = LinkMessage; - - #[inline] - fn from_xml(xml: &str) -> LinkMessage { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - let title = xmlutil::evaluate(&doc, "//xml/Title/text()").string(); - let description = xmlutil::evaluate(&doc, "//xml/Description/text()").string(); - let url = xmlutil::evaluate(&doc, "//xml/Url/text()").string(); - LinkMessage { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - title: title, - description: description, - url: url, - raw: xml.to_owned(), - } - } -} #[cfg(test)] mod tests { - use crate::wechat::{messages::MessageParser}; - use crate::wechat::mp::messages::MessageParser; + use crate::XmlMessageParser; use super::LinkMessage; #[test] @@ -62,12 +36,11 @@ mod tests { \ 1234567890123456\ "; - let msg = LinkMessage::from_xml(xml); + let msg = LinkMessage::from_xml(xml).unwrap(); assert_eq!("fromUser", &msg.source); assert_eq!("toUser", &msg.target); assert_eq!(1234567890123456, msg.id); - assert_eq!(1348831860, msg.time); assert_eq!("公众平台官网链接", &msg.title); assert_eq!("公众平台官网链接", &msg.description); assert_eq!("url", &msg.url); diff --git a/src/wechat/mp/messages/location.rs b/src/wechat/mp/messages/location.rs index 63d72c3..7d736d9 100644 --- a/src/wechat/mp/messages/location.rs +++ b/src/wechat/mp/messages/location.rs @@ -1,59 +1,29 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct LocationMessage { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, + #[serde(rename="Location_X")] pub location_x: f64, + #[serde(rename="Location_Y")] pub location_y: f64, - pub location: (f64, f64), + #[serde(rename="Scale")] pub scale: usize, + #[serde(rename="Label")] pub label: String, - pub raw: String, -} - -impl MessageParser for LocationMessage { - type WechatMessage = LocationMessage; - - #[inline] - fn from_xml(xml: &str) -> LocationMessage { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - let location_x = xmlutil::evaluate(&doc, "//xml/Location_X/text()").number(); - let location_y = xmlutil::evaluate(&doc, "//xml/Location_Y/text()").number(); - let scale = xmlutil::evaluate(&doc, "//xml/Scale/text()").number() as usize; - let label = xmlutil::evaluate(&doc, "//xml/Label/text()").string(); - LocationMessage { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - location_x: location_x, - location_y: location_y, - location: (location_x, location_y), - scale: scale, - label: label, - raw: xml.to_owned(), - } - } } #[cfg(test)] mod tests { - use crate::wechat::{messages::MessageParser}; - use crate::wechat::mp::messages::MessageParser; + use crate::XmlMessageParser; use super::LocationMessage; #[test] @@ -69,12 +39,11 @@ mod tests { 1234567890123456\ "; - let msg = LocationMessage::from_xml(xml); + let msg = LocationMessage::from_xml(xml).unwrap(); assert_eq!("fromUser", &msg.source); assert_eq!("toUser", &msg.target); assert_eq!(1234567890123456, msg.id); - assert_eq!(1348831860, msg.time); assert_eq!(23, msg.location_x as usize); assert_eq!(113, msg.location_y as usize); assert_eq!(20, msg.scale); diff --git a/src/wechat/mp/messages/mod.rs b/src/wechat/mp/messages/mod.rs index 1ffaff6..2d32bac 100644 --- a/src/wechat/mp/messages/mod.rs +++ b/src/wechat/mp/messages/mod.rs @@ -1,9 +1,3 @@ -pub trait MessageParser { - type WechatMessage; - - fn from_xml(xml: &str) -> Self::WechatMessage; -} - mod text; mod image; mod voice; @@ -13,8 +7,9 @@ mod location; mod link; mod unknown; -use crate::parse_message; -// export Message types +use crate::{LabradorResult, parse_message}; +use crate::events::{ClickEvent, LocationEvent, QualificationVerifySuccessEvent, ScanEvent, SubscribeEvent, SubscribeScanEvent, TemplateSendJobFinishEvent, UnsubscribeEvent, ViewEvent}; +// export MpMessage types pub use self::text::TextMessage; pub use self::image::ImageMessage; pub use self::voice::VoiceMessage; @@ -24,21 +19,11 @@ pub use self::location::LocationMessage; pub use self::link::LinkMessage; pub use self::unknown::UnknownMessage; -// export Event types -pub use super::events::SubscribeEvent; -pub use super::events::UnsubscribeEvent; -pub use super::events::ScanEvent; -pub use super::events::SubscribeScanEvent; -pub use super::events::LocationEvent; -pub use super::events::ClickEvent; -pub use super::events::ViewEvent; -pub use super::events::QualificationVerifySuccessEvent; -pub use super::events::TemplateSendJobFinishEvent; // an enum or messages and events #[allow(unused)] #[derive(Debug, Clone)] -pub enum Message { +pub enum MpMessage { TextMessage(TextMessage), ImageMessage(ImageMessage), VoiceMessage(VoiceMessage), @@ -59,52 +44,52 @@ pub enum Message { } #[allow(unused)] -impl Message { - pub fn parse>(xml: S) -> Message { +impl MpMessage { + pub fn parse>(xml: S) -> LabradorResult { parse_message(xml.as_ref()) } pub fn get_source(&self) -> String { match *self { - Message::TextMessage(ref msg) => msg.source.to_owned(), - Message::ImageMessage(ref msg) => msg.source.to_owned(), - Message::VoiceMessage(ref msg) => msg.source.to_owned(), - Message::ShortVideoMessage(ref msg) => msg.source.to_owned(), - Message::VideoMessage(ref msg) => msg.source.to_owned(), - Message::LocationMessage(ref msg) => msg.source.to_owned(), - Message::LinkMessage(ref msg) => msg.source.to_owned(), - Message::UnknownMessage(ref msg) => msg.source.to_owned(), - Message::SubscribeEvent(ref msg) => msg.source.to_owned(), - Message::UnsubscribeEvent(ref msg) => msg.source.to_owned(), - Message::SubscribeScanEvent(ref msg) => msg.source.to_owned(), - Message::ScanEvent(ref msg) => msg.source.to_owned(), - Message::LocationEvent(ref msg) => msg.source.to_owned(), - Message::ClickEvent(ref msg) => msg.source.to_owned(), - Message::ViewEvent(ref msg) => msg.source.to_owned(), - Message::TemplateSendJobFinishEvent(ref msg) => msg.source.to_owned(), - Message::QualificationVerifySuccessEvent(ref msg) => msg.source.to_owned(), + MpMessage::TextMessage(ref msg) => msg.source.to_string(), + MpMessage::ImageMessage(ref msg) => msg.source.to_string(), + MpMessage::VoiceMessage(ref msg) => msg.source.to_string(), + MpMessage::ShortVideoMessage(ref msg) => msg.source.to_string(), + MpMessage::VideoMessage(ref msg) => msg.source.to_string(), + MpMessage::LocationMessage(ref msg) => msg.source.to_string(), + MpMessage::LinkMessage(ref msg) => msg.source.to_string(), + MpMessage::UnknownMessage(ref msg) => msg.source.to_string(), + MpMessage::SubscribeEvent(ref msg) => msg.source.to_string(), + MpMessage::UnsubscribeEvent(ref msg) => msg.source.to_string(), + MpMessage::SubscribeScanEvent(ref msg) => msg.source.to_string(), + MpMessage::ScanEvent(ref msg) => msg.source.to_string(), + MpMessage::LocationEvent(ref msg) => msg.source.to_string(), + MpMessage::ClickEvent(ref msg) => msg.source.to_string(), + MpMessage::ViewEvent(ref msg) => msg.source.to_string(), + MpMessage::TemplateSendJobFinishEvent(ref msg) => msg.source.to_string(), + MpMessage::QualificationVerifySuccessEvent(ref msg) => msg.source.to_string(), } } pub fn get_target(&self) -> String { match *self { - Message::TextMessage(ref msg) => msg.target.to_owned(), - Message::ImageMessage(ref msg) => msg.target.to_owned(), - Message::VoiceMessage(ref msg) => msg.target.to_owned(), - Message::ShortVideoMessage(ref msg) => msg.target.to_owned(), - Message::VideoMessage(ref msg) => msg.target.to_owned(), - Message::LocationMessage(ref msg) => msg.target.to_owned(), - Message::LinkMessage(ref msg) => msg.target.to_owned(), - Message::UnknownMessage(ref msg) => msg.target.to_owned(), - Message::SubscribeEvent(ref msg) => msg.target.to_owned(), - Message::UnsubscribeEvent(ref msg) => msg.target.to_owned(), - Message::SubscribeScanEvent(ref msg) => msg.target.to_owned(), - Message::TemplateSendJobFinishEvent(ref msg) => msg.target.to_owned(), - Message::ScanEvent(ref msg) => msg.target.to_owned(), - Message::LocationEvent(ref msg) => msg.target.to_owned(), - Message::ClickEvent(ref msg) => msg.target.to_owned(), - Message::ViewEvent(ref msg) => msg.target.to_owned(), - Message::QualificationVerifySuccessEvent(ref msg) => msg.target.to_owned(), + MpMessage::TextMessage(ref msg) => msg.target.to_string(), + MpMessage::ImageMessage(ref msg) => msg.target.to_string(), + MpMessage::VoiceMessage(ref msg) => msg.target.to_string(), + MpMessage::ShortVideoMessage(ref msg) => msg.target.to_string(), + MpMessage::VideoMessage(ref msg) => msg.target.to_string(), + MpMessage::LocationMessage(ref msg) => msg.target.to_string(), + MpMessage::LinkMessage(ref msg) => msg.target.to_string(), + MpMessage::UnknownMessage(ref msg) => msg.target.to_string(), + MpMessage::SubscribeEvent(ref msg) => msg.target.to_string(), + MpMessage::UnsubscribeEvent(ref msg) => msg.target.to_string(), + MpMessage::SubscribeScanEvent(ref msg) => msg.target.to_string(), + MpMessage::TemplateSendJobFinishEvent(ref msg) => msg.target.to_string(), + MpMessage::ScanEvent(ref msg) => msg.target.to_string(), + MpMessage::LocationEvent(ref msg) => msg.target.to_string(), + MpMessage::ClickEvent(ref msg) => msg.target.to_string(), + MpMessage::ViewEvent(ref msg) => msg.target.to_string(), + MpMessage::QualificationVerifySuccessEvent(ref msg) => msg.target.to_string(), } } } diff --git a/src/wechat/mp/messages/shortvideo.rs b/src/wechat/mp/messages/shortvideo.rs index cd46f51..4c51288 100644 --- a/src/wechat/mp/messages/shortvideo.rs +++ b/src/wechat/mp/messages/shortvideo.rs @@ -1,51 +1,25 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; - -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct ShortVideoMessage { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, + #[serde(rename="MediaId")] pub media_id: String, + #[serde(rename="ThumbMediaId")] pub thumb_media_id: String, - pub raw: String, } -impl MessageParser for ShortVideoMessage { - type WechatMessage = ShortVideoMessage; - - #[inline] - fn from_xml(xml: &str) -> ShortVideoMessage { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - let media_id = xmlutil::evaluate(&doc, "//xml/MediaId/text()").string(); - let thumb_media_id = xmlutil::evaluate(&doc, "//xml/ThumbMediaId/text()").string(); - ShortVideoMessage { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - media_id: media_id, - thumb_media_id: thumb_media_id, - raw: xml.to_owned(), - } - } -} #[cfg(test)] mod tests { - use crate::wechat::{messages::MessageParser}; - use crate::wechat::mp::messages::MessageParser; + use crate::XmlMessageParser; use super::ShortVideoMessage; #[test] @@ -59,12 +33,11 @@ mod tests { \ 1234567890123456\ "; - let msg = ShortVideoMessage::from_xml(xml); + let msg = ShortVideoMessage::from_xml(xml).unwrap(); assert_eq!("fromUser", &msg.source); assert_eq!("toUser", &msg.target); assert_eq!(1234567890123456, msg.id); - assert_eq!(1348831860, msg.time); assert_eq!("media_id", &msg.media_id); assert_eq!("thumb_media_id", &msg.thumb_media_id); } diff --git a/src/wechat/mp/messages/text.rs b/src/wechat/mp/messages/text.rs index 1888aa4..c566e52 100644 --- a/src/wechat/mp/messages/text.rs +++ b/src/wechat/mp/messages/text.rs @@ -1,47 +1,23 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct TextMessage { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, + #[serde(rename="Content")] pub content: String, - pub raw: String, -} - -impl MessageParser for TextMessage { - type WechatMessage = TextMessage; - - #[inline] - fn from_xml(xml: &str) -> TextMessage { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - let content = xmlutil::evaluate(&doc, "//xml/Content/text()").string(); - TextMessage { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - content: content, - raw: xml.to_owned(), - } - } } #[cfg(test)] mod tests { - use crate::wechat::{messages::MessageParser}; + use crate::XmlMessageParser; use super::TextMessage; #[test] @@ -54,12 +30,11 @@ mod tests { \ 1234567890123456\ "; - let msg = TextMessage::from_xml(xml); + let msg = TextMessage::from_xml(xml).unwrap(); assert_eq!("fromUser", &msg.source); assert_eq!("toUser", &msg.target); assert_eq!(1234567890123456, msg.id); - assert_eq!(1348831860, msg.time); assert_eq!("this is a test", &msg.content); } } diff --git a/src/wechat/mp/messages/unknown.rs b/src/wechat/mp/messages/unknown.rs index 169b794..6de3e49 100644 --- a/src/wechat/mp/messages/unknown.rs +++ b/src/wechat/mp/messages/unknown.rs @@ -1,37 +1,14 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct UnknownMessage { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, - pub raw: String, -} - -impl MessageParser for UnknownMessage { - type WechatMessage = UnknownMessage; - - #[inline] - fn from_xml(xml: &str) -> UnknownMessage { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - UnknownMessage { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - raw: xml.to_owned(), - } - } } diff --git a/src/wechat/mp/messages/video.rs b/src/wechat/mp/messages/video.rs index 82cba08..0cccd95 100644 --- a/src/wechat/mp/messages/video.rs +++ b/src/wechat/mp/messages/video.rs @@ -1,50 +1,26 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct VideoMessage { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, + #[serde(rename="MediaId")] pub media_id: String, + #[serde(rename="ThumbMediaId")] pub thumb_media_id: String, - pub raw: String, } -impl MessageParser for VideoMessage { - type WechatMessage = VideoMessage; - - #[inline] - fn from_xml(xml: &str) -> VideoMessage { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - let media_id = xmlutil::evaluate(&doc, "//xml/MediaId/text()").string(); - let thumb_media_id = xmlutil::evaluate(&doc, "//xml/ThumbMediaId/text()").string(); - VideoMessage { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - media_id: media_id, - thumb_media_id: thumb_media_id, - raw: xml.to_owned(), - } - } -} #[cfg(test)] mod tests { - use crate::wechat::{messages::MessageParser}; + use crate::XmlMessageParser; use super::VideoMessage; #[test] @@ -58,12 +34,11 @@ mod tests { \ 1234567890123456\ "; - let msg = VideoMessage::from_xml(xml); + let msg = VideoMessage::from_xml(xml).unwrap(); assert_eq!("fromUser", &msg.source); assert_eq!("toUser", &msg.target); assert_eq!(1234567890123456, msg.id); - assert_eq!(1348831860, msg.time); assert_eq!("media_id", &msg.media_id); assert_eq!("thumb_media_id", &msg.thumb_media_id); } diff --git a/src/wechat/mp/messages/voice.rs b/src/wechat/mp/messages/voice.rs index 24b16f9..11476bc 100644 --- a/src/wechat/mp/messages/voice.rs +++ b/src/wechat/mp/messages/voice.rs @@ -1,53 +1,28 @@ -use chrono::NaiveDateTime; +use serde::{Serialize, Deserialize}; -use crate::wechat::mp::messages::MessageParser; -use crate::xmlutil; - -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct VoiceMessage { + #[serde(rename="FromUserName")] pub source: String, + #[serde(rename="ToUserName")] pub target: String, - pub time: i64, - pub create_time: NaiveDateTime, + #[serde(rename="CreateTime")] + pub create_time: i64, + #[serde(rename="MsgId")] pub id: i64, + #[serde(rename="MediaId")] pub media_id: String, + #[serde(rename="Format")] pub format: String, + #[serde(rename="Recognition")] pub recognition: String, - pub raw: String, } -impl MessageParser for VoiceMessage { - type WechatMessage = VoiceMessage; - - #[inline] - fn from_xml(xml: &str) -> VoiceMessage { - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let source = xmlutil::evaluate(&doc, "//xml/FromUserName/text()").string(); - let target = xmlutil::evaluate(&doc, "//xml/ToUserName/text()").string(); - let id = xmlutil::evaluate(&doc, "//xml/MsgId/text()").number() as i64; - let time = xmlutil::evaluate(&doc, "//xml/CreateTime/text()").number() as i64; - let media_id = xmlutil::evaluate(&doc, "//xml/MediaId/text()").string(); - let format = xmlutil::evaluate(&doc, "//xml/Format/text()").string(); - let recognition = xmlutil::evaluate(&doc, "//xml/Recognition/text()").string(); - VoiceMessage { - source: source, - target: target, - id: id, - time: time, - create_time: NaiveDateTime::from_timestamp(time, 0), - media_id: media_id, - format: format, - recognition: recognition, - raw: xml.to_owned(), - } - } -} #[cfg(test)] mod tests { - use crate::wechat::{messages::MessageParser}; + use crate::XmlMessageParser; use super::VoiceMessage; #[test] @@ -61,12 +36,11 @@ mod tests { \ 1234567890123456\ "; - let msg = VoiceMessage::from_xml(xml); + let msg = VoiceMessage::from_xml(xml).unwrap(); assert_eq!("fromUser", &msg.source); assert_eq!("toUser", &msg.target); assert_eq!(1234567890123456, msg.id); - assert_eq!(1348831860, msg.time); assert_eq!("media_id", &msg.media_id); assert_eq!("Format", &msg.format); assert_eq!("", &msg.recognition); diff --git a/src/wechat/mp/mod.rs b/src/wechat/mp/mod.rs index d06d264..fb0823f 100644 --- a/src/wechat/mp/mod.rs +++ b/src/wechat/mp/mod.rs @@ -10,8 +10,10 @@ pub mod messages; pub mod replies; #[allow(unused)] mod constants; +mod msg_parser; pub use api::*; +pub use msg_parser::parse_message; use crate::wechat::mp::constants::{ACCESS_TOKEN, APPID, CLIENT_CREDENTIAL, GRANT_TYPE, SECRET, TICKET_TYPE, TICKET_TYPE_JSAPI, TICKET_TYPE_SDK, TICKET_TYPE_WXCARD}; use crate::wechat::mp::method::WechatMpMethod::QrConnectUrl; @@ -254,8 +256,8 @@ impl WechatMpClient { /// 详情(http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN) /// pub fn check_signature(&self, signature: &str, timestamp: i64, nonce: &str) -> LabradorResult { - let crp = WechatCrypto::new(&self.aes_key.to_owned().unwrap_or_default()); - let _ = crp.check_signature(signature, timestamp, nonce, "", &self.token.to_owned().unwrap_or_default())?; + let crp = WechatCrypto::new(&self.aes_key.to_owned().unwrap_or_default()).token(&self.token.to_owned().unwrap_or_default()); + let _ = crp.check_signature(signature, timestamp, nonce, "")?; Ok(true) } diff --git a/src/wechat/mp/msg_parser.rs b/src/wechat/mp/msg_parser.rs new file mode 100644 index 0000000..7b92dfe --- /dev/null +++ b/src/wechat/mp/msg_parser.rs @@ -0,0 +1,48 @@ +use crate::messages::{ImageMessage, LinkMessage, LocationMessage, MpMessage, ShortVideoMessage, TextMessage, UnknownMessage, VideoMessage, VoiceMessage}; +use crate::{LabradorResult, XmlMessageParser, xmlutil}; +use crate::events::{ClickEvent, LocationEvent, QualificationVerifySuccessEvent, ScanEvent, SubscribeEvent, SubscribeScanEvent, UnsubscribeEvent, ViewEvent}; + +pub fn parse_message>(xml: S) -> LabradorResult { + let xml = xml.as_ref(); + let package = xmlutil::parse(xml); + let doc = package.as_document(); + let msg_type_str = xmlutil::evaluate(&doc, "//xml/MsgType/text()").string().to_lowercase(); + let msg_type = &msg_type_str[..]; + let msg = match msg_type { + "text" => MpMessage::TextMessage(TextMessage::from_xml(xml)?), + "image" => MpMessage::ImageMessage(ImageMessage::from_xml(xml)?), + "voice" => MpMessage::VoiceMessage(VoiceMessage::from_xml(xml)?), + "shortvideo" => MpMessage::ShortVideoMessage(ShortVideoMessage::from_xml(xml)?), + "video" => MpMessage::VideoMessage(VideoMessage::from_xml(xml)?), + "location" => MpMessage::LocationMessage(LocationMessage::from_xml(xml)?), + "link" => MpMessage::LinkMessage(LinkMessage::from_xml(xml)?), + "event" => { + let event_str = xmlutil::evaluate(&doc, "//xml/Event/text()").string().to_lowercase(); + if &event_str == "subscribe" { + let event_key = xmlutil::evaluate(&doc, "//xml/EventKey/text()").string(); + if &event_key != "" { + // special SubscribeScanEvent + return Ok(MpMessage::SubscribeScanEvent(SubscribeScanEvent::from_xml(xml)?)); + } + } + parse_event(&event_str[..], xml)? + }, + _ => MpMessage::UnknownMessage(UnknownMessage::from_xml(xml)?), + }; + Ok(msg) +} + +fn parse_event(event: &str, xml: &str) -> LabradorResult { + let msg =match event { + "subscribe" => MpMessage::SubscribeEvent(SubscribeEvent::from_xml(xml)?), + "unsubscribe" => MpMessage::UnsubscribeEvent(UnsubscribeEvent::from_xml(xml)?), + "templatesendjobfinish" => MpMessage::UnsubscribeEvent(UnsubscribeEvent::from_xml(xml)?), + "scan" => MpMessage::ScanEvent(ScanEvent::from_xml(xml)?), + "location" => MpMessage::LocationEvent(LocationEvent::from_xml(xml)?), + "click" => MpMessage::ClickEvent(ClickEvent::from_xml(xml)?), + "view" => MpMessage::ViewEvent(ViewEvent::from_xml(xml)?), + "qualification_verify_success" => MpMessage::QualificationVerifySuccessEvent(QualificationVerifySuccessEvent::from_xml(xml)?), + _ => MpMessage::UnknownMessage(UnknownMessage::from_xml(xml)?), + }; + Ok(msg) +} diff --git a/src/wechat/msg_parser.rs b/src/wechat/msg_parser.rs deleted file mode 100644 index ae756ce..0000000 --- a/src/wechat/msg_parser.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::messages::{Message, MessageParser}; -use crate::{messages, xmlutil}; - -pub fn parse_message>(xml: S) -> Message { - let xml = xml.as_ref(); - let package = xmlutil::parse(xml); - let doc = package.as_document(); - let msg_type_str = xmlutil::evaluate(&doc, "//xml/MsgType/text()").string().to_lowercase(); - let msg_type = &msg_type_str[..]; - let msg = match msg_type { - "text" => Message::TextMessage(messages::TextMessage::from_xml(xml)), - "image" => Message::ImageMessage(messages::ImageMessage::from_xml(xml)), - "voice" => Message::VoiceMessage(messages::VoiceMessage::from_xml(xml)), - "shortvideo" => Message::ShortVideoMessage(messages::ShortVideoMessage::from_xml(xml)), - "video" => Message::VideoMessage(messages::VideoMessage::from_xml(xml)), - "location" => Message::LocationMessage(messages::LocationMessage::from_xml(xml)), - "link" => Message::LinkMessage(messages::LinkMessage::from_xml(xml)), - "event" => { - let event_str = xmlutil::evaluate(&doc, "//xml/Event/text()").string().to_lowercase(); - if &event_str == "subscribe" { - let event_key = xmlutil::evaluate(&doc, "//xml/EventKey/text()").string(); - if &event_key != "" { - // special SubscribeScanEvent - return Message::SubscribeScanEvent(messages::SubscribeScanEvent::from_xml(xml)); - } - } - parse_event(&event_str[..], xml) - }, - _ => Message::UnknownMessage(messages::UnknownMessage::from_xml(xml)), - }; - msg -} - -fn parse_event(event: &str, xml: &str) -> Message { - match event { - "subscribe" => Message::SubscribeEvent(messages::SubscribeEvent::from_xml(xml)), - "unsubscribe" => Message::UnsubscribeEvent(messages::UnsubscribeEvent::from_xml(xml)), - "templatesendjobfinish" => Message::UnsubscribeEvent(messages::UnsubscribeEvent::from_xml(xml)), - "scan" => Message::ScanEvent(messages::ScanEvent::from_xml(xml)), - "location" => Message::LocationEvent(messages::LocationEvent::from_xml(xml)), - "click" => Message::ClickEvent(messages::ClickEvent::from_xml(xml)), - "view" => Message::ViewEvent(messages::ViewEvent::from_xml(xml)), - "qualification_verify_success" => Message::QualificationVerifySuccessEvent(messages::QualificationVerifySuccessEvent::from_xml(xml)), - _ => Message::UnknownMessage(messages::UnknownMessage::from_xml(xml)), - } -} diff --git a/src/wechat/pay/api/wxpay.rs b/src/wechat/pay/api/wxpay.rs index 2ba7577..6ec47c2 100644 --- a/src/wechat/pay/api/wxpay.rs +++ b/src/wechat/pay/api/wxpay.rs @@ -604,7 +604,7 @@ mod tests { let mut private_key = Vec::new(); File::open("src/wechat/pay/sec/apiclient_key.pem").unwrap().read_to_end(&mut private_key).unwrap(); let r = rt.spawn(async { - let c = WechatPayClient::new("appid", "secret"); + let c = WechatPayClient::::new("appid", "secret"); let mut client =c.wxpay(); let result = client.close_order_v3(WechatCloseOrderRequestV3 { mchid: "mchid".to_string(), @@ -630,7 +630,7 @@ mod tests { let mut private_key = Vec::new(); File::open("src/wechat/pay/sec/apiclient_key.pem").unwrap().read_to_end(&mut private_key).unwrap(); let r = rt.spawn(async { - let c = WechatPayClient::new("appid", "secret"); + let c = WechatPayClient::::new("appid", "secret"); let mut client =c.wxpay(); // .cert(MchCert { // mch_id: "1602920235".to_string().into(), @@ -682,7 +682,7 @@ mod tests { let mut private_key = Vec::new(); File::open("src/wechat/pay/sec/apiclient_key.pem").unwrap().read_to_end(&mut private_key).unwrap(); let r = rt.spawn(async { - let c = WechatPayClient::new("appid", "secret"); + let c = WechatPayClient::::new("appid", "secret"); let mut client =c.wxpay(); let date = Local::now().to_rfc3339_opts(SecondsFormat::Secs, false); let result = client.unified_order_v3(TradeType::Jsapi, WechatPayRequestV3 { From 499a66cacb9fd44f861374aeadadb721154b7df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=8D=E6=99=A8?= Date: Thu, 15 Sep 2022 17:13:30 +0800 Subject: [PATCH 5/9] =?UTF-8?q?=E8=B0=83=E6=95=B4openssl=20feature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 73cef88..16468e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ serde-xml-rs = "0.6.0" rustc-serialize = "^0.3" serde_urlencoded = "0.7.1" urlencoding = "2.1.0" -openssl = { version = "0.10.41", features = ["vendored"] } +openssl = { version = "0.10.41" } tracing = "0.1" dashmap = "5.3.4" json = {version = "0.12.4", optional= true } From 030e66f569fd297f4ee938522f7eb57eb3553ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=8D=E6=99=A8?= Date: Thu, 15 Sep 2022 17:46:44 +0800 Subject: [PATCH 6/9] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=201.=E5=A2=9E=E5=8A=A0=E4=BC=81=E5=BE=AE-?= =?UTF-8?q?=E6=98=8E=E6=96=87corpid=E8=BD=AC=E6=8D=A2=E4=B8=BA=E5=8A=A0?= =?UTF-8?q?=E5=AF=86corpid=202.=E5=A2=9E=E5=8A=A0=E4=BC=81=E5=BE=AE-?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E5=BA=94=E7=94=A8=E4=BA=8C=E7=BB=B4=E7=A0=81?= =?UTF-8?q?=203.=E5=A2=9E=E5=8A=A0=E4=BC=81=E5=BE=AE-=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E7=9A=84=E7=AE=A1=E7=90=86=E5=91=98=E5=88=97?= =?UTF-8?q?=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/wechat/cp/method.rs | 8 ++++ src/wechat/cp/tp/mod.rs | 104 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/src/wechat/cp/method.rs b/src/wechat/cp/method.rs index 6275e5d..3793ea7 100644 --- a/src/wechat/cp/method.rs +++ b/src/wechat/cp/method.rs @@ -10,11 +10,15 @@ pub enum WechatCpMethod { JsCode2Session, GetPermanentCode, GetPreAuthCode, + CorpToOpenCorpid, + GetAppQrcode, + SetSessionInfo, GetJsapiTicket, GetAgentConfigTicket, GetSuiteJsapiTicket, GetCallbackIp, GetAuthInfo, + GetAdminInfo, GetOrder, GetOrderList, Media(CpMediaMethod), @@ -40,7 +44,11 @@ impl RequestMethod for WechatCpMethod { WechatCpMethod::GetOrder => String::from("/cgi-bin/service/get_order"), WechatCpMethod::GetOrderList => String::from("/cgi-bin/service/get_order_list"), WechatCpMethod::GetPreAuthCode => String::from("/cgi-bin/service/get_pre_auth_code"), + WechatCpMethod::SetSessionInfo => String::from("/cgi-bin/service/set_session_info"), WechatCpMethod::GetAuthInfo => String::from("/cgi-bin/service/get_auth_info"), + WechatCpMethod::GetAdminInfo => String::from("/cgi-bin/service/get_admin_list"), + WechatCpMethod::CorpToOpenCorpid => String::from("/cgi-bin/service/corpid_to_opencorpid"), + WechatCpMethod::GetAppQrcode => String::from("/cgi-bin/service/get_app_qrcode"), WechatCpMethod::GetPermanentCode => String::from("/cgi-bin/service/get_permanent_code"), WechatCpMethod::GetProviderToken => String::from("/cgi-bin/service/get_provider_token"), WechatCpMethod::GetCorpToken => String::from("/cgi-bin/service/get_corp_token"), diff --git a/src/wechat/cp/tp/mod.rs b/src/wechat/cp/tp/mod.rs index 317f751..201851c 100644 --- a/src/wechat/cp/tp/mod.rs +++ b/src/wechat/cp/tp/mod.rs @@ -1,4 +1,4 @@ - +use bytes::Bytes; use serde::{Serialize, Deserialize}; use serde_json::{json, Value}; @@ -349,7 +349,7 @@ impl WechatCpTpClient { let req = json!({ "auth_code": auth_code, }); - let result = self.client.post(WechatCpMethod::GetPermanentCode, vec![], req, RequestType::Json).await?.json::()?; + let result = self.post(WechatCpMethod::GetPermanentCode, vec![], req, RequestType::Json).await?.json::()?; WechatCommonResponse::parse::(result) } @@ -357,7 +357,7 @@ impl WechatCpTpClient { /// 获取预授权链接 /// pub async fn get_pre_auth_url(&self, redirect_uri: &str, state: Option<&str>) -> LabradorResult { - let result = self.client.get(WechatCpMethod::GetPreAuthCode, vec![], RequestType::Json).await?.json::()?; + let result = self.get(WechatCpMethod::GetPreAuthCode, vec![], RequestType::Json).await?.json::()?; let mut pre_auth_url = format!("{}?suite_id={}&pre_auth_code={}&redirect_uri={}", AUTH_URL_INSTALL, self.suite_id.to_owned().unwrap_or_default(), result.pre_auth_code, urlencoding::encode(redirect_uri)); if let Some(state) = state { pre_auth_url.push_str(&format!("&state={}", state)); @@ -365,6 +365,21 @@ impl WechatCpTpClient { Ok(pre_auth_url) } + ///
    +    /// 设置授权配置
    +    /// 
    + pub async fn set_session_info(&self, pre_auth_code: &str, app_ids: Vec<&str>, auth_type: u8) -> LabradorResult { + let req = json!({ + "pre_auth_code": pre_auth_code, + "session_info": + { + "appid": app_ids, + "auth_type": auth_type + } + }); + self.post(WechatCpMethod::SetSessionInfo, vec![], req, RequestType::Json).await?.json::() + } + ///
         /// 获取企业的授权信息
         /// 
    @@ -377,6 +392,78 @@ impl WechatCpTpClient { WechatCommonResponse::parse::(result) } + + ///
    +    /// 获取应用的管理员列表
    +    /// 第三方服务商可以用此接口获取授权企业中某个第三方应用的管理员列表(不包括外部管理员),以便服务商在用户进入应用主页之后根据是否管理员身份做权限的区分。
    +    /// 
    + pub async fn get_admin_info_with_agentid(&self, agent_id: i32) -> LabradorResult> { + let req = json!({ + "auth_corpid": auth_corp_id, + "agentid": agent_id + }); + let result = self.client.post(WechatCpMethod::GetAdminInfo, vec![], req, RequestType::Json).await?.json::()?; + let v = WechatCommonResponse::parse::(result)?; + serde_json::from_value::>(v["admin"].to_owned()).map_err(LabraError::from) + } + + + ///
    +    /// 获取应用二维码
    +    /// 用于获取第三方应用二维码。
    +    /// 
    + pub async fn get_app_qrcode_buffer(&self, suite_id: &str, appid: Option, state: Option<&str>, style: Option) -> LabradorResult { + let req = json!({ + "suite_id": suite_id, + "appid": appid, + "state": state, + "style": style, + "result_type": 1 + }); + self.client.post(WechatCpMethod::GetAppQrcode, vec![], req, RequestType::Json).await?.bytes() + } + + ///
    +    /// 获取应用二维码
    +    /// 用于获取第三方应用二维码。
    +    /// 
    + pub async fn get_app_qrcode_url(&self, suite_id: &str, appid: Option, state: Option<&str>, style: Option, result_type: Option) -> LabradorResult { + let req = json!({ + "suite_id": suite_id, + "appid": appid, + "state": state, + "style": style, + "result_type": 2 + }); + let v = self.client.post(WechatCpMethod::GetAppQrcode, vec![], req, RequestType::Json).await?.json::()?; + let v = WechatCommonResponse::parse::(v)?; + let qrcode = v["qrcode"].as_str().unwrap_or_default(); + Ok(qrcode.to_string()) + } + + ///
    +    /// 明文corpid转换为加密corpid
    +    /// 为更好地保护企业与用户的数据,第三方应用获取的corpid不再是明文的corpid,将升级为第三方服务商级别的加密corpid(了解更多)。第三方可以将已有的明文corpid转换为第三方的加密corpid。。
    +    /// 
    + pub async fn corpid_to_opencorpid(&self, corpid: &str) -> LabradorResult { + let req = json!({ + "corpid": corpid, + }); + let v = self.client.post(WechatCpMethod::CorpToOpenCorpid, vec![], req, RequestType::Json).await?.json::()?; + let v = WechatCommonResponse::parse::(v)?; + let qrcode = v["open_corpid"].as_str().unwrap_or_default(); + Ok(qrcode.to_string()) + } + + + ///
    +    /// 获取应用的管理员列表
    +    /// 第三方服务商可以用此接口获取授权企业中某个第三方应用的管理员列表(不包括外部管理员),以便服务商在用户进入应用主页之后根据是否管理员身份做权限的区分。
    +    /// 
    + pub async fn get_admin_info(&self) -> LabradorResult> { + self.get_admin_info_with_agentid(self.agent_id.unwrap_or_default()).await + } + /// ///
         /// 创建机构级jsApiTicket签名
    @@ -546,6 +633,17 @@ pub struct AuthUserInfo {
     }
     
     
    +/// 管理员信息
    +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    +pub struct AdminUserInfo {
    +    pub userid: Option,
    +    /// 管理员的open_userid,可能为空
    +    pub open_userid: Option,
    +    /// 该管理员对应用的权限:0=发消息权限,1=管理权限
    +    pub auth_type: Option,
    +}
    +
    +
     /// 授权信息
     #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
     pub struct AuthInfo {
    
    From d9c9ddb894ab3b6b40567705c33a37fcdb299d90 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?=E9=BE=8D=E6=99=A8?= 
    Date: Thu, 15 Sep 2022 18:35:42 +0800
    Subject: [PATCH 7/9] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B4=A6=E5=8F=B7?=
     =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=9A=84=E9=97=AE=E9=A2=98?=
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    ---
     src/wechat/cp/tp/license.rs | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/src/wechat/cp/tp/license.rs b/src/wechat/cp/tp/license.rs
    index 3b08da4..b5ccc53 100644
    --- a/src/wechat/cp/tp/license.rs
    +++ b/src/wechat/cp/tp/license.rs
    @@ -345,7 +345,7 @@ pub struct WechatCpTpLicenseOrder {
     pub struct WechatCpTpLicenseOrderAccountListResponse {
         pub next_cursor: Option,
         pub has_more: Option,
    -    pub account_list: Option,
    +    pub account_list: Option>,
     }
     
     /// 订单账号信息
    
    From 028fe22670c42b7046418102bade5f4f7e80cfe4 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?=E9=BE=8D=E6=99=A8?= 
    Date: Thu, 15 Sep 2022 18:48:02 +0800
    Subject: [PATCH 8/9] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B4=A6=E5=8F=B7?=
     =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=9A=84=E9=97=AE=E9=A2=98?=
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    ---
     src/wechat/cp/tp/mod.rs | 11 +----------
     1 file changed, 1 insertion(+), 10 deletions(-)
    
    diff --git a/src/wechat/cp/tp/mod.rs b/src/wechat/cp/tp/mod.rs
    index 201851c..325f3d9 100644
    --- a/src/wechat/cp/tp/mod.rs
    +++ b/src/wechat/cp/tp/mod.rs
    @@ -397,7 +397,7 @@ impl WechatCpTpClient {
         /// 获取应用的管理员列表
         /// 第三方服务商可以用此接口获取授权企业中某个第三方应用的管理员列表(不包括外部管理员),以便服务商在用户进入应用主页之后根据是否管理员身份做权限的区分。
         /// 
    - pub async fn get_admin_info_with_agentid(&self, agent_id: i32) -> LabradorResult> { + pub async fn get_admin_info(&self, auth_corp_id: &str, agent_id: i32) -> LabradorResult> { let req = json!({ "auth_corpid": auth_corp_id, "agentid": agent_id @@ -455,15 +455,6 @@ impl WechatCpTpClient { Ok(qrcode.to_string()) } - - ///
    -    /// 获取应用的管理员列表
    -    /// 第三方服务商可以用此接口获取授权企业中某个第三方应用的管理员列表(不包括外部管理员),以便服务商在用户进入应用主页之后根据是否管理员身份做权限的区分。
    -    /// 
    - pub async fn get_admin_info(&self) -> LabradorResult> { - self.get_admin_info_with_agentid(self.agent_id.unwrap_or_default()).await - } - /// ///
         /// 创建机构级jsApiTicket签名
    
    From 31852e5081eeab3f868de360fbfed448d077584d Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?=E9=BE=8D=E6=99=A8?= 
    Date: Fri, 16 Sep 2022 08:54:04 +0800
    Subject: [PATCH 9/9] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=AE=B8=E5=8F=AF?=
     =?UTF-8?q?=E8=AF=81=E6=BF=80=E6=B4=BB/=E6=94=AF=E4=BB=98/=E9=80=80?=
     =?UTF-8?q?=E6=AC=BE=E5=9B=9E=E8=B0=83=E6=B6=88=E6=81=AF=E4=BA=8B=E4=BB=B6?=
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    ---
     src/wechat/cp/events/auto_activate.rs        | 44 ++++++++++++++++++++
     src/wechat/cp/events/licecnse_pay_success.rs | 17 ++++++++
     src/wechat/cp/events/licecnse_refund.rs      | 17 ++++++++
     src/wechat/cp/events/mod.rs                  | 10 ++++-
     src/wechat/cp/events/unlicensed.rs           | 19 +++++++++
     src/wechat/cp/messages/mod.rs                |  5 ++-
     6 files changed, 110 insertions(+), 2 deletions(-)
     create mode 100644 src/wechat/cp/events/auto_activate.rs
     create mode 100644 src/wechat/cp/events/licecnse_pay_success.rs
     create mode 100644 src/wechat/cp/events/licecnse_refund.rs
     create mode 100644 src/wechat/cp/events/unlicensed.rs
    
    diff --git a/src/wechat/cp/events/auto_activate.rs b/src/wechat/cp/events/auto_activate.rs
    new file mode 100644
    index 0000000..fec168b
    --- /dev/null
    +++ b/src/wechat/cp/events/auto_activate.rs
    @@ -0,0 +1,44 @@
    +use serde::{Serialize, Deserialize};
    +
    +/// 自动激活回调通知
    +/// 企业成员满足自动激活条件并触发自动激活后,企业微信后台会推送“自动激活通知”到服务商的系统事件接收URL(应用管理 -通用开发参数-系统事件接收URL)。
    +#[derive(Debug, Serialize, Deserialize, Clone)]
    +pub struct CpAutoActivateEvent {
    +    #[serde(rename="AuthCorpId")]
    +    pub auth_corp_id: String,
    +    #[serde(rename="ServiceCorpId")]
    +    pub service_corp_id: String,
    +    #[serde(rename="TimeStamp")]
    +    pub time_stamp: i64,
    +    #[serde(rename="InfoType")]
    +    pub into_type: String,
    +    #[serde(rename="OrderId")]
    +    pub order_id: String,
    +    /// 许可自动激活的时机,1:企业成员主动访问应用,2:服务商调用消息推送接口,3:服务商调用互通接口
    +    #[serde(rename="Scene")]
    +    pub scene: i64,
    +    #[serde(rename="AccountList")]
    +    pub account_list: Option,
    +}
    +
    +#[derive(Debug, Serialize, Deserialize, Clone)]
    +pub struct AccountList {
    +    /// 自动激活的许可帐号激活码
    +    #[serde(rename="ActiveCode")]
    +    pub active_code: String,
    +    /// 自动激活的许可的类型,1:基础许可,2:互通许可
    +    #[serde(rename="Type")]
    +    pub account_type: u8,
    +    /// 自动激活后,该许可的到期时间
    +    #[serde(rename="ExpireTime")]
    +    pub expire_time: i64,
    +    /// 许可自动激活的成员的UserID
    +    #[serde(rename="UserId")]
    +    pub user_id: String,
    +    /// 激活成员自动激活前的许可状态,1:未激活许可,2:已激活许可且许可未过期(即许可的剩余时长小于等于7天),3:已激活许可且许可已过期
    +    #[serde(rename="PreviousStatus")]
    +    pub previous_status: Option,
    +    /// 仅针对已激活的成员进行自动激活时返回,返回该成员之前激活的旧的激活码
    +    #[serde(rename="PreviousActiveCode")]
    +    pub previous_active_code: Option,
    +}
    diff --git a/src/wechat/cp/events/licecnse_pay_success.rs b/src/wechat/cp/events/licecnse_pay_success.rs
    new file mode 100644
    index 0000000..84e966b
    --- /dev/null
    +++ b/src/wechat/cp/events/licecnse_pay_success.rs
    @@ -0,0 +1,17 @@
    +use serde::{Serialize, Deserialize};
    +
    +/// 支付成功通知
    +/// 当服务商购买接口调用许可帐号并完成付款后,企业微信后台会推送“支付成功通知”到服务商的系统事件接收URL(应用管理 -通用开发参数-系统事件接收URL)。
    +#[derive(Debug, Serialize, Deserialize, Clone)]
    +pub struct CpLicensePaySuccessEvent {
    +    #[serde(rename="AuthCorpId")]
    +    pub auth_corp_id: String,
    +    #[serde(rename="ServiceCorpId")]
    +    pub service_corp_id: String,
    +    #[serde(rename="InfoType")]
    +    pub into_type: String,
    +    #[serde(rename="OrderId")]
    +    pub order_id: String,
    +    #[serde(rename="TimeStamp")]
    +    pub timestamp: i64,
    +}
    diff --git a/src/wechat/cp/events/licecnse_refund.rs b/src/wechat/cp/events/licecnse_refund.rs
    new file mode 100644
    index 0000000..926a49a
    --- /dev/null
    +++ b/src/wechat/cp/events/licecnse_refund.rs
    @@ -0,0 +1,17 @@
    +use serde::{Serialize, Deserialize};
    +
    +/// 支付成功通知
    +/// 当服务商购买接口调用许可帐号并完成付款后,企业微信后台会推送“支付成功通知”到服务商的系统事件接收URL(应用管理 -通用开发参数-系统事件接收URL)。
    +#[derive(Debug, Serialize, Deserialize, Clone)]
    +pub struct CpLicenseRefundEvent {
    +    #[serde(rename="AuthCorpId")]
    +    pub auth_corp_id: String,
    +    #[serde(rename="ServiceCorpId")]
    +    pub service_corp_id: String,
    +    #[serde(rename="InfoType")]
    +    pub into_type: String,
    +    #[serde(rename="OrderId")]
    +    pub order_id: String,
    +    #[serde(rename="TimeStamp")]
    +    pub timestamp: i64,
    +}
    diff --git a/src/wechat/cp/events/mod.rs b/src/wechat/cp/events/mod.rs
    index cdebe2c..eb7703e 100644
    --- a/src/wechat/cp/events/mod.rs
    +++ b/src/wechat/cp/events/mod.rs
    @@ -21,6 +21,10 @@ mod auth;
     mod permanent_code;
     mod app_admin;
     mod tp_contact_change;
    +mod unlicensed;
    +mod licecnse_pay_success;
    +mod licecnse_refund;
    +mod auto_activate;
     
     pub use subscribe::CpSubscribeEvent;
     pub use unsubscribe::CpUnsubscribeEvent;
    @@ -37,4 +41,8 @@ pub use template_card::*;
     pub use auth::*;
     pub use permanent_code::*;
     pub use app_admin::*;
    -pub use tp_contact_change::*;
    \ No newline at end of file
    +pub use tp_contact_change::*;
    +pub use unlicensed::*;
    +pub use licecnse_pay_success::*;
    +pub use licecnse_refund::*;
    +pub use auto_activate::*;
    \ No newline at end of file
    diff --git a/src/wechat/cp/events/unlicensed.rs b/src/wechat/cp/events/unlicensed.rs
    new file mode 100644
    index 0000000..6900288
    --- /dev/null
    +++ b/src/wechat/cp/events/unlicensed.rs
    @@ -0,0 +1,19 @@
    +use serde::{Serialize, Deserialize};
    +
    +/// 接口许可失效通知
    +/// 当许可帐号失效(未激活或已过期)的企业成员访问应用或小程序时,企业微信会提示用户联系服务商开通许可帐号,同时企业微信自动推送该通知事件至服务商后台。接收消息的方式参见使用接收消息。
    +#[derive(Debug, Serialize, Deserialize, Clone)]
    +pub struct CpUnlicensedNotifyEvent {
    +    #[serde(rename="FromUserName")]
    +    pub source: String,
    +    #[serde(rename="ToUserName")]
    +    pub target: String,
    +    #[serde(rename="CreateTime")]
    +    pub create_time: i64,
    +    #[serde(rename="Event")]
    +    pub event: String,
    +    #[serde(rename="MsgType")]
    +    pub mst_type: String,
    +    #[serde(rename="AgentID")]
    +    pub agent_id: i64,
    +}
    diff --git a/src/wechat/cp/messages/mod.rs b/src/wechat/cp/messages/mod.rs
    index d2dc2e6..6e83110 100644
    --- a/src/wechat/cp/messages/mod.rs
    +++ b/src/wechat/cp/messages/mod.rs
    @@ -11,7 +11,7 @@ mod location;
     mod link;
     mod unknown;
     
    -use crate::{CpAppAdminChangeEvent, CpAuthCancelEvent, CpAuthChangeEvent, CpAuthCreateEvent, CpBatchJobResultEvent, CpContactCreatePartyEvent, CpContactCreateUserEvent, CpContactDeletePartyEvent, CpContactDeleteUserEvent, CpContactUpdatePartyEvent, CpContactUpdateTagEvent, CpContactUpdateUserEvent, CpEnterAgentEvent, CpLocationEvent, CpMenuClickEvent, CpMenuLocationSelectEvent, CpMenuPicPhotoOrAlbumEvent, CpMenuPicSysPhotoEvent, CpMenuPicWeixinEvent, CpMenuScanCodePushEvent, CpMenuScanCodeWaitMsgEvent, CpMenuViewEvent, CpOpenApprovalChangeEvent, CpPermanentCodeEvent, CpShareAgentChangeEvent, CpShareChainChangeEvent, CpSubscribeEvent, CpTemplateCardEvent, CpTemplateCardMenuEvent, CpTicketEvent, CpTpContactCreatePartyEvent, CpTpContactCreateUserEvent, CpTpContactDeletePartyEvent, CpTpContactDeleteUserEvent, CpTpContactUpdatePartyEvent, CpTpContactUpdateTagEvent, CpTpContactUpdateUserEvent, CpUnsubscribeEvent, LabradorResult, parse_cp_message};
    +use crate::{CpAppAdminChangeEvent, CpAuthCancelEvent, CpAuthChangeEvent, CpAuthCreateEvent, CpBatchJobResultEvent, CpContactCreatePartyEvent, CpContactCreateUserEvent, CpContactDeletePartyEvent, CpContactDeleteUserEvent, CpContactUpdatePartyEvent, CpContactUpdateTagEvent, CpContactUpdateUserEvent, CpEnterAgentEvent, CpLicensePaySuccessEvent, CpLicenseRefundEvent, CpLocationEvent, CpMenuClickEvent, CpMenuLocationSelectEvent, CpMenuPicPhotoOrAlbumEvent, CpMenuPicSysPhotoEvent, CpMenuPicWeixinEvent, CpMenuScanCodePushEvent, CpMenuScanCodeWaitMsgEvent, CpMenuViewEvent, CpOpenApprovalChangeEvent, CpPermanentCodeEvent, CpShareAgentChangeEvent, CpShareChainChangeEvent, CpSubscribeEvent, CpTemplateCardEvent, CpTemplateCardMenuEvent, CpTicketEvent, CpTpContactCreatePartyEvent, CpTpContactCreateUserEvent, CpTpContactDeletePartyEvent, CpTpContactDeleteUserEvent, CpTpContactUpdatePartyEvent, CpTpContactUpdateTagEvent, CpTpContactUpdateUserEvent, CpUnlicensedNotifyEvent, CpUnsubscribeEvent, LabradorResult, parse_cp_message};
     // export Message types
     pub use self::text::CpTextMessage;
     pub use self::image::CpImageMessage;
    @@ -56,6 +56,9 @@ pub enum CpMessage {
         TpContactDeletePartyEvent(CpTpContactDeletePartyEvent),
         TpContactUpdateTagEvent(CpTpContactUpdateTagEvent),
         EnterAgentEvent(CpEnterAgentEvent),
    +    LicensePaySuccessEvent(CpLicensePaySuccessEvent),
    +    LicenseRefundEvent(CpLicenseRefundEvent),
    +    UnlicensedNotifyEvent(CpUnlicensedNotifyEvent),
         MenuClickEvent(CpMenuClickEvent),
         MenuViewEvent(CpMenuViewEvent),
         MenuPicWeixinEvent(CpMenuPicWeixinEvent),