Skip to main content

wx_sdk/mp/
menu.rs

1#![allow(non_camel_case_types)]
2
3use crate::{
4    error::{CommonError, CommonResponse, SdkError},
5    wechat::WxApiRequestBuilder,
6};
7
8use super::SdkResult;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Serialize, Deserialize)]
12pub enum BtnKeyType {
13    click,
14    scancode_waitmsg,
15    scancode_push,
16    pic_sysphoto,
17    pic_photo_or_album,
18    pic_weixin,
19    location_select,
20}
21
22#[derive(Debug, Serialize, Deserialize)]
23pub struct BtnKey {
24    #[serde(rename = "type")]
25    pub type_: BtnKeyType,
26    pub name: String,
27    pub key: String,
28}
29
30#[derive(Debug, Serialize, Deserialize)]
31pub enum BtnUrlType {
32    view,
33}
34
35#[derive(Debug, Serialize, Deserialize)]
36pub struct BtnUrl {
37    #[serde(rename = "type")]
38    pub type_: BtnUrlType,
39    pub name: String,
40    pub url: String,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
44pub enum BtnMediaType {
45    /// 图片
46    media_id,
47    /// 图文消息
48    view_limited,
49}
50
51#[derive(Debug, Serialize, Deserialize)]
52pub struct BtnMedia {
53    #[serde(rename = "type")]
54    pub type_: BtnMediaType,
55    pub name: String,
56    #[serde(alias = "value")]
57    pub media_id: String,
58}
59
60#[derive(Debug, Serialize, Deserialize)]
61pub struct BtnValue {
62    #[serde(rename = "type")]
63    pub type_: BtnMediaType,
64    pub name: String,
65    pub value: String,
66}
67
68#[derive(Debug, Serialize, Deserialize)]
69pub struct BtnMiniprogram {
70    pub type_: String,
71    pub name: String,
72    pub url: String,
73    pub appid: String,
74    pub pagepath: String,
75}
76
77/// 层级菜单
78#[derive(Debug, Serialize, Deserialize)]
79pub struct SubBtn {
80    pub name: String,
81    pub sub_button: Vec<Btn>,
82}
83
84#[derive(Debug, Serialize, Deserialize)]
85#[serde(untagged)]
86pub enum Btn {
87    url(BtnUrl),
88    key(BtnKey),
89    media(BtnMedia),
90    miniprogram(BtnMiniprogram),
91    sub(SubBtn),
92}
93
94#[derive(Debug, Serialize, Deserialize)]
95pub struct MenuInfo {
96    pub is_menu_open: i8,
97    pub selfmenu_info: SelfmenuInfo,
98}
99
100#[derive(Debug, Serialize, Deserialize)]
101pub struct SelfmenuInfo {
102    pub button: Vec<ButtonInfo2>,
103}
104
105#[derive(Debug, Serialize, Deserialize)]
106#[serde(untagged)]
107pub enum ButtonInfo {
108    url(BtnUrl),
109    key(BtnKey),
110    media(BtnMedia),
111    miniprogram(BtnMiniprogram),
112    sub(SubButtonList),
113}
114/// 查询接口是这种结构,但是创建接口不是
115#[derive(Debug, Serialize, Deserialize)]
116pub struct SubButtonList {
117    name: String,
118    sub_button: SubButtonInfo,
119}
120
121/// 查询接口是这种结构,但是创建接口不是
122#[derive(Debug, Serialize, Deserialize)]
123pub struct SubButtonInfo {
124    list: Vec<ButtonInfo>,
125}
126
127#[derive(Debug, Serialize, Deserialize, Clone)]
128pub struct ButtonInfo2 {
129    #[serde(rename = "type")]
130    pub type_: Option<String>,
131    pub name: String,
132    pub value: Option<String>,
133    pub url: Option<String>,
134    pub key: Option<String>,
135    pub appid: Option<String>,
136    pub pagepath: Option<String>,
137    pub sub_button: Option<SubButtonInfo2>,
138}
139
140#[derive(Debug, Serialize, Deserialize, Clone)]
141pub struct SubButtonInfo2 {
142    pub list: Vec<ButtonInfo2>,
143}
144
145#[derive(Debug, Serialize, Deserialize)]
146pub struct MatchRule {
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub tag_id: Option<i32>,
149
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub sex: Option<i32>,
152
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub country: Option<String>,
155
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub province: Option<String>,
158
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub city: Option<String>,
161
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub client_platform_type: Option<u8>,
164
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub language: Option<String>,
167}
168
169impl MatchRule {
170    fn is_valid(&self) -> bool {
171        match (
172            self.tag_id,
173            self.sex,
174            self.country.as_ref(),
175            self.province.as_ref(),
176            self.city.as_ref(),
177            self.client_platform_type,
178            self.language.as_ref(),
179        ) {
180            (None, None, None, None, None, None, None) => false,
181            (_, _, None, Some(_), _, _, _) => false,
182            (_, _, _, None, Some(_), _, _) => false,
183            _ => true,
184        }
185    }
186}
187
188#[derive(Debug, Serialize, Deserialize, Clone)]
189#[serde(untagged)]
190pub enum MenuButton {
191    button(ButtonItem),
192    root_button(RootButton),
193}
194#[derive(Debug, Serialize, Deserialize, Clone)]
195#[serde(tag = "type")]
196pub enum ButtonItem {
197    view(ButtonView),
198    click(ButtonClick),
199    miniprogram(ButtonMiniProgram),
200    scancode_waitmsg(ButtonClick),
201    scancode_push(ButtonClick),
202    pic_sysphoto(ButtonClick),
203    pic_photo_or_album(ButtonClick),
204    pic_weixin(ButtonClick),
205    location_select(ButtonClick),
206    media_id(ButtonMedia),
207    view_limited(ButtonMedia),
208}
209
210impl From<ButtonItem> for MenuButton {
211    fn from(btns: ButtonItem) -> Self {
212        MenuButton::button(btns)
213    }
214}
215
216#[derive(Debug, Serialize, Deserialize, Clone)]
217pub struct RootButton {
218    pub name: String,
219    pub sub_button: Vec<ButtonItem>,
220}
221
222impl From<RootButton> for MenuButton {
223    fn from(r_btn: RootButton) -> Self {
224        MenuButton::root_button(r_btn)
225    }
226}
227
228#[derive(Debug, Serialize, Deserialize, Clone)]
229pub struct ButtonView {
230    pub name: String,
231    pub url: String,
232}
233
234#[derive(Debug, Serialize, Deserialize, Clone)]
235pub struct ButtonClick {
236    pub name: String,
237    pub key: String,
238}
239
240#[derive(Debug, Serialize, Deserialize, Clone)]
241pub struct ButtonMiniProgram {
242    pub name: String,
243    pub url: String,
244    pub appid: String,
245    pub pagepath: String,
246}
247#[derive(Debug, Serialize, Deserialize, Clone)]
248pub struct ButtonMedia {
249    pub name: String,
250    pub media_id: String,
251}
252
253#[derive(Debug, Serialize, Deserialize, Clone)]
254pub struct MenuId {
255    pub menuid: String,
256}
257
258#[derive(Debug, Serialize, Deserialize)]
259pub struct MatchButtons {
260    pub button: Vec<MenuButton>,
261
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub menuid: Option<u32>,
264
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub matchrule: Option<MatchRule>,
267}
268
269#[derive(Debug, Serialize, Deserialize)]
270pub struct AllButtons {
271    pub menu: MatchButtons,
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub conditionalmenu: Option<Vec<MatchButtons>>,
274}
275
276/// 菜单模块
277pub struct MenuModule<'a, T: WxApiRequestBuilder>(pub(crate) &'a T);
278
279impl<'a, T: WxApiRequestBuilder> MenuModule<'a, T> {
280    /// 创建自定义菜单
281    pub async fn create(&self, menu: Vec<Btn>) -> SdkResult<()> {
282        let base_url = "https://api.weixin.qq.com/cgi-bin/menu/create";
283        let sdk = self.0;
284        let builder = sdk.wx_post(base_url).await?;
285        let res: CommonError = builder
286            .json(&serde_json::json!({ "button": menu }))
287            .send()
288            .await?
289            .json()
290            .await?;
291
292        res.into()
293    }
294
295    /// 创建自定义菜单(通过自定义的json数据)
296    pub async fn create_by_json<U: Serialize + ?Sized>(&self, menu_json: &U) -> SdkResult<()> {
297        let base_url = "https://api.weixin.qq.com/cgi-bin/menu/create";
298        let sdk = self.0;
299        let res: CommonError = sdk
300            .wx_post(base_url)
301            .await?
302            .json(menu_json)
303            .send()
304            .await?
305            .json()
306            .await?;
307
308        res.into()
309    }
310    /// 获取当前菜单信息
311    pub async fn get_current_selfmenu_info(&self) -> SdkResult<MenuInfo> {
312        let base_url = "https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info";
313        let sdk = self.0;
314        let res: CommonResponse<MenuInfo> =
315            sdk.wx_get(base_url).await?.send().await?.json().await?;
316
317        res.into()
318    }
319
320    /// 删除自定义菜单
321    /// 调用此接口会删除默认菜单及全部个性化菜单
322    pub async fn delete(&self) -> SdkResult<()> {
323        let base_url = "https://api.weixin.qq.com/cgi-bin/menu/delete";
324        let sdk = self.0;
325        let res: CommonError = sdk.wx_get(base_url).await?.send().await?.json().await?;
326        res.into()
327    }
328
329    /// 添加个性化菜单
330    pub async fn addconditional(
331        &self,
332        rules: MatchRule,
333        menu_json: Vec<MenuButton>,
334    ) -> SdkResult<MenuId> {
335        let base_url = "https://api.weixin.qq.com/cgi-bin/menu/addconditional";
336
337        if !rules.is_valid() {
338            return Err(SdkError::InvalidParams(
339                "add conditional menu match rules invalid.".to_string(),
340            ));
341        }
342        let sdk = self.0;
343        let builder = sdk.wx_post(base_url).await?;
344        let res: CommonResponse<MenuId> = builder
345            .json(&serde_json::json!({
346                "button": &menu_json,
347                "matchrule": rules
348            }))
349            .send()
350            .await?
351            .json()
352            .await?;
353
354        res.into()
355    }
356
357    /// 删除个性化菜单
358    pub async fn delconditional(&self, menuid: MenuId) -> SdkResult<()> {
359        let base_url = "https://api.weixin.qq.com/cgi-bin/menu/delconditional";
360        let sdk = self.0;
361        let builder = sdk.wx_post(base_url).await?;
362        let msg: CommonError = builder.json(&menuid).send().await?.json().await?;
363
364        msg.into()
365    }
366
367    /// 测试个性化菜单匹配结果
368    /// user_id可以是粉丝的OpenID,也可以是粉丝的微信号。
369    pub async fn trymatch(&self, user_id: String) -> SdkResult<MatchButtons> {
370        let base_url = "https://api.weixin.qq.com/cgi-bin/menu/trymatch";
371        let sdk = self.0;
372        let builder = sdk.wx_post(base_url).await?;
373        let msg: CommonResponse<MatchButtons> = builder
374            .json(&serde_json::json!({ "user_id": &user_id }))
375            .send()
376            .await?
377            .json()
378            .await?;
379
380        msg.into()
381    }
382    /// 获取自定义菜单配置
383    /// 在设置了个性化菜单后,使用本自定义菜单查询接口可以获取默认菜单和全部个性化菜单信息
384    pub async fn get(&self) -> SdkResult<AllButtons> {
385        let base_url = "https://api.weixin.qq.com/cgi-bin/menu/get";
386        let sdk = self.0;
387        let res: CommonResponse<AllButtons> =
388            sdk.wx_get(base_url).await?.send().await?.json().await?;
389
390        res.into()
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use std::mem::discriminant;
398    // use serde::{Serialize, Deserialize};
399
400    #[test]
401    fn deserialize_menu() -> std::result::Result<(), &'static str> {
402        let menu_json = "
403    [{
404        \"type\": \"view\",
405        \"name\": \"今日歌曲\",
406        \"url\": \"V1001_TODAY_MUSIC\"
407    }, {
408      \"name\": \"菜单\",
409      \"sub_button\": [
410        {
411          \"type\": \"scancode_push\",
412          \"name\": \"扫码推事件\",
413          \"key\": \"rselfmenu_0_1\"
414        }, {
415          \"type\": \"media_id\",
416          \"name\": \"图片\",
417          \"media_id\": \"V1001_GOOD\"
418        }]
419    }]
420       ";
421        // }, {
422        //     \"type\": \"miniprogram\",
423        //     \"name\": \"wxa\",
424        //     \"url\": \"http://mp.weixin.qq.com\",
425        //     \"appid\": \"wx286b93c14bbf93aa\",
426        //     \"pagepath\": \"pages/lunar/index\"
427
428        let menu: Vec<Btn> = serde_json::from_str(&menu_json).unwrap();
429        // println!("{:#?}", &menu);
430
431        match &menu[0] {
432            Btn::url(btn) => {
433                assert_eq!(discriminant(&btn.type_), discriminant(&BtnUrlType::view));
434            }
435            _ => return Err("match &menu[0]"),
436        }
437        match &menu[1] {
438            Btn::sub(btn) => {
439                assert_eq!(btn.name.as_str(), "菜单");
440                match &btn.sub_button[0] {
441                    Btn::key(btn) => {
442                        assert_eq!(
443                            discriminant(&btn.type_),
444                            discriminant(&BtnKeyType::scancode_push)
445                        );
446                    }
447                    _ => return Err("match &btn.sub_button[0]"),
448                }
449                match &btn.sub_button[1] {
450                    Btn::media(btn) => {
451                        assert_eq!(
452                            discriminant(&btn.type_),
453                            discriminant(&BtnMediaType::media_id)
454                        );
455                    }
456                    _ => return Err("match &btn.sub_button[1]"),
457                }
458            }
459            _ => return Err("match &menu[1]"),
460        }
461
462        Ok(())
463    }
464
465    #[test]
466    fn deserialize_allmenu1() -> std::result::Result<(), Box<dyn std::error::Error>> {
467        let input = r#"{
468    "menu": {
469        "button": [
470            {
471                "type": "click", 
472                "name": "今日歌曲", 
473                "key": "V1001_TODAY_MUSIC", 
474                "sub_button": [ ]
475            }, 
476            {
477                "type": "click", 
478                "name": "歌手简介", 
479                "key": "V1001_TODAY_SINGER", 
480                "sub_button": [ ]
481            }, 
482            {
483                "name": "菜单", 
484                "sub_button": [
485                    {
486                        "type": "view", 
487                        "name": "搜索", 
488                        "url": "http://www.soso.com/", 
489                        "sub_button": [ ]
490                    }, 
491                    {
492                        "type": "view", 
493                        "name": "视频", 
494                        "url": "http://v.qq.com/", 
495                        "sub_button": [ ]
496                    }, 
497                    {
498                        "type": "click", 
499                        "name": "赞一下我们", 
500                        "key": "V1001_GOOD", 
501                        "sub_button": [ ]
502                    }
503                ]
504            }
505        ]
506    }
507}"#;
508        let _menu: AllButtons = serde_json::from_str(&input).unwrap();
509        // println!("{:#?}", &_menu);
510        Ok(())
511    }
512
513    #[test]
514    fn deserialize_allmenu2() -> std::result::Result<(), Box<dyn std::error::Error>> {
515        let input = r#"{"menu": {
516                    "button": [
517                        {
518                            "type": "click", 
519                            "name": "今日歌曲", 
520                            "key": "V1001_TODAY_MUSIC", 
521                            "sub_button": [ ]
522                        }
523                    ], 
524                    "menuid": 208396938
525                }, 
526                "conditionalmenu": [
527                    {
528                        "button": [
529                            {
530                                "type": "click", 
531                                "name": "今日歌曲", 
532                                "key": "V1001_TODAY_MUSIC", 
533                                "sub_button": [ ]
534                            }, 
535                            {
536                                "name": "菜单", 
537                                "sub_button": [
538                                    {
539                                        "type": "view", 
540                                        "name": "搜索", 
541                                        "url": "http://www.soso.com/", 
542                                        "sub_button": [ ]
543                                    }, 
544                                    {
545                                        "type": "view", 
546                                        "name": "视频", 
547                                        "url": "http://v.qq.com/", 
548                                        "sub_button": [ ]
549                                    }, 
550                                    {
551                                        "type": "click", 
552                                        "name": "赞一下我们", 
553                                        "key": "V1001_GOOD", 
554                                        "sub_button": [ ]
555                                    }
556                                ]
557                            }
558                        ], 
559                        "matchrule": {
560                            "group_id": 2, 
561                            "sex": 1, 
562                            "country": "中国", 
563                            "province": "广东", 
564                            "city": "广州", 
565                            "client_platform_type": 2
566                        }, 
567                        "menuid": 208396993
568                    }
569                ]
570}"#;
571        let _menu: AllButtons = serde_json::from_str(&input).unwrap();
572        // println!("{:#?}", &_menu);
573        Ok(())
574    }
575}