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 media_id,
47 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#[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#[derive(Debug, Serialize, Deserialize)]
116pub struct SubButtonList {
117 name: String,
118 sub_button: SubButtonInfo,
119}
120
121#[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
276pub struct MenuModule<'a, T: WxApiRequestBuilder>(pub(crate) &'a T);
278
279impl<'a, T: WxApiRequestBuilder> MenuModule<'a, T> {
280 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 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 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 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 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 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 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 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 #[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 let menu: Vec<Btn> = serde_json::from_str(&menu_json).unwrap();
429 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 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 Ok(())
574 }
575}