async_wechat/official_account/
message.rs

1use chrono::Utc;
2use quick_xml::{de::from_str, se};
3use serde::{Deserialize, Serialize};
4use std::pin::Pin;
5
6use actix_web::{
7    FromRequest, HttpRequest,
8    dev::Payload,
9    error,
10    web::{self},
11};
12
13use super::signature::signature;
14
15pub struct MsgType;
16
17impl MsgType {
18    // TEXT 表示文本消息
19    pub const TEXT: &str = "text";
20    // IMAGE 表示图片消息
21    pub const IMAGE: &str = "image";
22    // VOICE 表示语音消息
23    pub const VOICE: &str = "voice";
24    // VIDEO 表示视频消息
25    pub const VIDEO: &str = "video";
26    // MINIPROGRAMPAGE 表示小程序卡片消息
27    pub const MINIPROGRAMPAGE: &str = "miniprogrampage";
28    // SHORTVIDEO 表示短视频消息 [限接收]
29    pub const SHORTVIDEO: &str = "shortvideo";
30    // LOCATION 表示坐标消息 [限接收]
31    pub const LOCATION: &str = "location";
32    // LINK 表示链接消息 [限接收]
33    pub const LINK: &str = "link";
34    // MUSIC 表示音乐消息 [限回复]
35    pub const MUSIC: &str = "music";
36    // NEWS 表示图文消息 [限回复]
37    pub const NEWS: &str = "news";
38    // TRANSFER 表示消息消息转发到客服
39    pub const TRANSFER: &str = "transfer_customer_service";
40    // EVENT 表示事件推送消息
41    pub const EVENT: &str = "event";
42}
43
44pub struct EventType;
45
46impl EventType {
47    // SUBSCRIBE 订阅
48    pub const SUBSCRIBE: &str = "subscribe";
49    // UNSUBSCRIBE 取消订阅
50    pub const UNSUBSCRIBE: &str = "unsubscribe";
51    // SCAN 取消订阅
52    pub const SCAN: &str = "SCAN";
53}
54
55#[derive(Debug, Serialize, Deserialize)]
56#[serde(rename = "xml")]
57pub struct WechatMessage {
58    #[serde(rename = "ToUserName")]
59    pub to_user_name: String, // 微信号(公众号原始id)
60    #[serde(rename = "FromUserName")]
61    pub from_user_name: String, // 发送方账号(一个OpenID)
62    #[serde(rename = "CreateTime")]
63    pub create_time: u64, // 消息创建时间 (整型)
64    #[serde(rename = "MsgType")]
65    pub msg_type: String, // 消息类型,文本为text
66    #[serde(rename = "Content")]
67    pub content: Option<String>, // 文本消息内容
68    #[serde(rename = "MsgId")]
69    pub msg_id: Option<u64>, // 消息id,64位整型
70    #[serde(rename = "Idx")]
71    pub idx: Option<u64>, // 多图文时第几篇文章,从1开始(消息如果来自文章时才有)
72    #[serde(rename = "Event")]
73    pub event: Option<String>, // 事件类型,subscribe
74    #[serde(rename = "EventKey")]
75    pub event_key: Option<String>, // 事件KEY值,qrscene_为前缀,后面为二维码的场景值ID
76    #[serde(rename = "Ticket")]
77    pub ticket: Option<String>, // 二维码的ticket,可用来换取二维码图片
78}
79
80#[derive(Debug, Serialize, Deserialize)]
81#[serde(rename = "xml")]
82pub struct WeChatResponse {
83    #[serde(rename = "ToUserName")]
84    pub to_user_name: String,
85    #[serde(rename = "FromUserName")]
86    pub from_user_name: String,
87    #[serde(rename = "CreateTime")]
88    pub create_time: u64,
89    #[serde(rename = "MsgType")]
90    pub msg_type: String,
91    #[serde(rename = "Content")]
92    pub content: String,
93}
94
95pub struct MessageHandler {
96    pub message: WechatMessage,
97}
98
99impl MessageHandler {
100    pub fn to_string(
101        &self,
102        message: &WeChatResponse,
103    ) -> Result<String, Box<dyn std::error::Error>> {
104        match se::to_string(message) {
105            Ok(s) => Ok(s),
106            Err(e) => Err(e.into()),
107        }
108    }
109}
110
111impl WechatMessage {
112    pub fn get_open_id(&self) -> String {
113        self.from_user_name.clone()
114    }
115
116    pub fn get_gh_id(&self) -> String {
117        self.to_user_name.clone()
118    }
119
120    pub fn plaintext(&self, content: &str) -> WeChatResponse {
121        let now = Utc::now().naive_utc();
122        WeChatResponse {
123            to_user_name: self.from_user_name.clone(),
124            from_user_name: self.to_user_name.clone(),
125            create_time: now.and_utc().timestamp() as u64,
126            msg_type: MsgType::TEXT.to_string(),
127            content: content.to_string(),
128        }
129    }
130}
131
132#[derive(Debug, Serialize, Deserialize)]
133pub struct WechatQuery {
134    timestamp: String,
135    nonce: String,
136    signature: String,
137    echostr: Option<String>, // 验证时才需要
138}
139
140impl FromRequest for MessageHandler {
141    type Error = actix_web::Error;
142    type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
143
144    fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
145        let fut = web::Bytes::from_request(req, payload);
146
147        let query_future = web::Query::<WechatQuery>::from_query(req.query_string())
148            .map(|q| q.into_inner())
149            .map_err(|e| {
150                actix_web::error::ErrorBadRequest(format!(
151                    "Failed to parse query parameters: {}",
152                    e
153                ))
154            });
155
156        Box::pin(async move {
157            let query = match query_future {
158                Ok(q) => q,
159                Err(e) => return Err(e),
160            };
161
162            // TODO: token use official account
163            if query.signature != signature("wechat", &query.timestamp, &query.nonce) {
164                return Err(error::ErrorUnauthorized("Invalid signature"));
165            }
166
167            let bytes = fut.await?;
168            let xml_str = String::from_utf8_lossy(&bytes);
169
170            let message = from_str::<WechatMessage>(&xml_str)
171                .map_err(|e| error::ErrorBadRequest(format!("Invalid XML input: {}", e)))?;
172
173            Ok(MessageHandler { message })
174        })
175    }
176}