1use std::collections::HashMap;
36use std::sync::Arc;
37
38use serde::{Deserialize, Serialize};
39
40use super::{WechatApi, WechatContext};
41use crate::error::WechatError;
42use crate::types::OpenId;
43
44pub type SubscribeMessageData = HashMap<String, SubscribeMessageValue>;
46
47#[derive(Debug, Clone, Serialize)]
49pub struct SubscribeMessageValue {
50 pub value: String,
51}
52
53impl SubscribeMessageValue {
54 pub fn new(value: impl Into<String>) -> Self {
56 Self {
57 value: value.into(),
58 }
59 }
60}
61
62#[derive(Debug, Clone, Serialize)]
64struct SubscribeMessageRequest {
65 #[serde(rename = "touser")]
66 touser: OpenId,
67 #[serde(rename = "template_id")]
68 template_id: String,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 page: Option<String>,
71 data: SubscribeMessageData,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 miniprogram_state: Option<MiniProgramState>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 lang: Option<Lang>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79#[serde(rename_all = "lowercase")]
80pub enum MiniProgramState {
81 Developer,
82 Trial,
83 Formal,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
87pub enum Lang {
88 #[serde(rename = "zh_CN")]
89 ZhCN,
90 #[serde(rename = "en_US")]
91 EnUS,
92 #[serde(rename = "zh_HK")]
93 ZhHK,
94 #[serde(rename = "zh_TW")]
95 ZhTW,
96}
97
98#[derive(Debug, Clone)]
100pub struct SubscribeMessageOptions {
101 pub touser: OpenId,
103 pub template_id: String,
105 pub data: SubscribeMessageData,
107 pub page: Option<String>,
109 pub miniprogram_state: Option<MiniProgramState>,
111 pub lang: Option<Lang>,
113}
114
115#[derive(Debug, Clone, Deserialize)]
117struct SubscribeMessageResponse {
118 #[serde(default)]
119 errcode: i32,
120 #[serde(default)]
121 errmsg: String,
122}
123
124#[non_exhaustive]
126#[derive(Debug, Clone, Deserialize)]
127pub struct TemplateInfo {
128 #[serde(rename = "priTmplId")]
130 pub private_template_id: String,
131 pub title: String,
133 pub content: String,
135 #[serde(default)]
137 pub example: Option<String>,
138 #[serde(rename = "type")]
140 pub template_type: i32,
141}
142
143#[non_exhaustive]
145#[derive(Debug, Clone, Deserialize)]
146pub struct TemplateListResponse {
147 pub data: Vec<TemplateInfo>,
148 #[serde(default)]
149 errcode: i32,
150 #[serde(default)]
151 errmsg: String,
152}
153
154#[derive(Debug, Clone, Serialize)]
156struct AddTemplateRequest {
157 tid: String,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 kid_list: Option<Vec<i32>>,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 scene_desc: Option<String>,
162}
163
164#[non_exhaustive]
166#[derive(Debug, Clone, Deserialize)]
167pub struct AddTemplateResponse {
168 #[serde(rename = "priTmplId")]
169 pub private_template_id: String,
170 #[serde(default)]
171 errcode: i32,
172 #[serde(default)]
173 errmsg: String,
174}
175
176#[non_exhaustive]
178#[derive(Debug, Clone, Deserialize)]
179pub struct CategoryInfo {
180 pub id: i32,
182 pub name: String,
184}
185
186#[non_exhaustive]
188#[derive(Debug, Clone, Deserialize)]
189pub struct CategoryListResponse {
190 pub data: Vec<CategoryInfo>,
191 #[serde(default)]
192 errcode: i32,
193 #[serde(default)]
194 errmsg: String,
195}
196
197#[non_exhaustive]
198#[derive(Debug, Clone, Deserialize, Serialize)]
199pub struct PubTemplateKeywordInfo {
200 #[serde(default)]
201 pub kid: i32,
202 #[serde(default)]
203 pub name: String,
204 #[serde(default)]
205 pub rule: String,
206}
207
208#[non_exhaustive]
209#[derive(Debug, Clone, Deserialize, Serialize)]
210pub struct PubTemplateKeywordResponse {
211 #[serde(default)]
212 pub data: Vec<PubTemplateKeywordInfo>,
213 #[serde(default)]
214 pub(crate) errcode: i32,
215 #[serde(default)]
216 pub(crate) errmsg: String,
217}
218
219#[non_exhaustive]
220#[derive(Debug, Clone, Deserialize, Serialize)]
221pub struct PubTemplateTitleInfo {
222 #[serde(default)]
223 pub tid: String,
224 #[serde(default)]
225 pub title: String,
226 #[serde(default)]
227 pub r#type: i32,
228 #[serde(default)]
229 pub category_id: i32,
230}
231
232#[non_exhaustive]
233#[derive(Debug, Clone, Deserialize, Serialize)]
234pub struct PubTemplateTitleListResponse {
235 #[serde(default)]
236 pub data: Vec<PubTemplateTitleInfo>,
237 #[serde(default)]
238 pub(crate) errcode: i32,
239 #[serde(default)]
240 pub(crate) errmsg: String,
241}
242
243#[non_exhaustive]
244#[derive(Debug, Clone, Serialize)]
245pub struct UserNotifyRequest {
246 #[serde(flatten)]
247 pub payload: HashMap<String, serde_json::Value>,
248}
249
250#[non_exhaustive]
251#[derive(Debug, Clone, Serialize)]
252pub struct UserNotifyExtRequest {
253 #[serde(flatten)]
254 pub payload: HashMap<String, serde_json::Value>,
255}
256
257#[non_exhaustive]
258#[derive(Debug, Clone, Serialize)]
259pub struct GetUserNotifyRequest {
260 #[serde(flatten)]
261 pub payload: HashMap<String, serde_json::Value>,
262}
263
264#[non_exhaustive]
265#[derive(Debug, Clone, Deserialize, Serialize)]
266pub struct UserNotifyResponse {
267 #[serde(default)]
268 pub(crate) errcode: i32,
269 #[serde(default)]
270 pub(crate) errmsg: String,
271 #[serde(flatten)]
272 pub extra: HashMap<String, serde_json::Value>,
273}
274
275pub struct SubscribeApi {
279 context: Arc<WechatContext>,
280}
281
282impl SubscribeApi {
283 pub fn new(context: Arc<WechatContext>) -> Self {
285 Self { context }
286 }
287
288 pub async fn send(&self, options: SubscribeMessageOptions) -> Result<(), WechatError> {
317 let request = SubscribeMessageRequest {
318 touser: options.touser,
319 template_id: options.template_id,
320 page: options.page,
321 data: options.data,
322 miniprogram_state: options.miniprogram_state,
323 lang: options.lang,
324 };
325
326 let response: SubscribeMessageResponse = self
327 .context
328 .authed_post("/cgi-bin/message/subscribe/send", &request)
329 .await?;
330
331 WechatError::check_api(response.errcode, &response.errmsg)?;
332
333 Ok(())
334 }
335
336 pub async fn add_template(
355 &self,
356 tid: &str,
357 kid_list: Option<Vec<i32>>,
358 scene_desc: Option<&str>,
359 ) -> Result<String, WechatError> {
360 let request = AddTemplateRequest {
361 tid: tid.to_string(),
362 kid_list,
363 scene_desc: scene_desc.map(|s| s.to_string()),
364 };
365
366 let response: AddTemplateResponse = self
367 .context
368 .authed_post("/wxaapi/newtmpl/addtemplate", &request)
369 .await?;
370
371 WechatError::check_api(response.errcode, &response.errmsg)?;
372
373 Ok(response.private_template_id)
374 }
375
376 pub async fn get_template_list(&self) -> Result<Vec<TemplateInfo>, WechatError> {
392 let response: TemplateListResponse = self
393 .context
394 .authed_get("/wxaapi/newtmpl/gettemplate", &[])
395 .await?;
396
397 WechatError::check_api(response.errcode, &response.errmsg)?;
398
399 Ok(response.data)
400 }
401
402 pub async fn delete_template(&self, pri_tmpl_id: &str) -> Result<(), WechatError> {
415 #[derive(Serialize)]
416 struct Request {
417 #[serde(rename = "pri_tmpl_id")]
418 pri_tmpl_id: String,
419 }
420
421 let response: SubscribeMessageResponse = self
422 .context
423 .authed_post(
424 "/wxaapi/newtmpl/deltemplate",
425 &Request {
426 pri_tmpl_id: pri_tmpl_id.to_string(),
427 },
428 )
429 .await?;
430
431 WechatError::check_api(response.errcode, &response.errmsg)?;
432
433 Ok(())
434 }
435
436 pub async fn get_category(&self) -> Result<Vec<CategoryInfo>, WechatError> {
452 let response: CategoryListResponse = self
453 .context
454 .authed_get("/wxaapi/newtmpl/getcategory", &[])
455 .await?;
456
457 WechatError::check_api(response.errcode, &response.errmsg)?;
458
459 Ok(response.data)
460 }
461
462 pub async fn get_pub_template_keywords_by_id(
463 &self,
464 tid: &str,
465 ) -> Result<PubTemplateKeywordResponse, WechatError> {
466 let response: PubTemplateKeywordResponse = self
467 .context
468 .authed_get("/wxaapi/newtmpl/getpubtemplatekeywords", &[("tid", tid)])
469 .await?;
470
471 WechatError::check_api(response.errcode, &response.errmsg)?;
472
473 Ok(response)
474 }
475
476 pub async fn get_pub_template_title_list(
477 &self,
478 ids: &[i32],
479 start: i32,
480 limit: i32,
481 ) -> Result<PubTemplateTitleListResponse, WechatError> {
482 let ids_text = ids
483 .iter()
484 .map(std::string::ToString::to_string)
485 .collect::<Vec<String>>()
486 .join(",");
487 let start_text = start.to_string();
488 let limit_text = limit.to_string();
489 let response: PubTemplateTitleListResponse = self
490 .context
491 .authed_get(
492 "/wxaapi/newtmpl/getpubtemplatetitles",
493 &[
494 ("ids", ids_text.as_str()),
495 ("start", start_text.as_str()),
496 ("limit", limit_text.as_str()),
497 ],
498 )
499 .await?;
500
501 WechatError::check_api(response.errcode, &response.errmsg)?;
502
503 Ok(response)
504 }
505
506 pub async fn set_user_notify(
507 &self,
508 request: &UserNotifyRequest,
509 ) -> Result<UserNotifyResponse, WechatError> {
510 self.post_user_notify("/cgi-bin/message/update_template_card", request)
511 .await
512 }
513
514 pub async fn set_user_notify_ext(
515 &self,
516 request: &UserNotifyExtRequest,
517 ) -> Result<UserNotifyResponse, WechatError> {
518 self.post_user_notify("/cgi-bin/message/update_template_card_ext", request)
519 .await
520 }
521
522 pub async fn get_user_notify(
523 &self,
524 request: &GetUserNotifyRequest,
525 ) -> Result<UserNotifyResponse, WechatError> {
526 self.post_user_notify("/cgi-bin/message/get_template_card", request)
527 .await
528 }
529
530 async fn post_user_notify<B: Serialize>(
531 &self,
532 endpoint: &str,
533 body: &B,
534 ) -> Result<UserNotifyResponse, WechatError> {
535 let response: UserNotifyResponse = self.context.authed_post(endpoint, body).await?;
536
537 WechatError::check_api(response.errcode, &response.errmsg)?;
538
539 Ok(response)
540 }
541}
542
543impl WechatApi for SubscribeApi {
544 fn context(&self) -> &WechatContext {
545 &self.context
546 }
547
548 fn api_name(&self) -> &'static str {
549 "subscribe"
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556 use crate::client::WechatClient;
557 use crate::token::TokenManager;
558 use crate::types::{AppId, AppSecret};
559
560 fn create_test_context(base_url: &str) -> Arc<WechatContext> {
561 let appid = AppId::new("wx1234567890abcdef").unwrap();
562 let secret = AppSecret::new("secret1234567890ab").unwrap();
563 let client = Arc::new(
564 WechatClient::builder()
565 .appid(appid)
566 .secret(secret)
567 .base_url(base_url)
568 .build()
569 .unwrap(),
570 );
571 let token_manager = Arc::new(TokenManager::new((*client).clone()));
572 Arc::new(WechatContext::new(client, token_manager))
573 }
574
575 #[test]
576 fn test_subscribe_message_value() {
577 let value = SubscribeMessageValue::new("test value");
578 assert_eq!(value.value, "test value");
579 }
580
581 #[test]
582 fn test_subscribe_message_data() {
583 let mut data = SubscribeMessageData::new();
584 data.insert(
585 "thing1".to_string(),
586 SubscribeMessageValue::new("Order #123"),
587 );
588 data.insert(
589 "time2".to_string(),
590 SubscribeMessageValue::new("2024-01-01"),
591 );
592
593 assert_eq!(data.len(), 2);
594 assert_eq!(data.get("thing1").unwrap().value, "Order #123");
595 }
596
597 #[test]
598 fn test_subscribe_message_options() {
599 let mut data = SubscribeMessageData::new();
600 data.insert("key1".to_string(), SubscribeMessageValue::new("value1"));
601
602 let options = SubscribeMessageOptions {
603 touser: OpenId::new("o6_bmjrPTlm6_2sgVt7hMZOPfL2M").unwrap(),
604 template_id: "template_id_456".to_string(),
605 data,
606 page: Some("pages/index/index".to_string()),
607 miniprogram_state: Some(MiniProgramState::Developer),
608 lang: Some(Lang::ZhCN),
609 };
610
611 assert_eq!(options.touser.as_str(), "o6_bmjrPTlm6_2sgVt7hMZOPfL2M");
612 assert_eq!(options.template_id, "template_id_456");
613 assert_eq!(options.page, Some("pages/index/index".to_string()));
614 }
615
616 #[test]
617 fn test_pub_template_keywords_response_parse() {
618 let json = r#"{
619 "data": [{"kid": 1, "name": "thing1", "rule": "20个以内字符"}],
620 "errcode": 0,
621 "errmsg": "ok"
622 }"#;
623
624 let response: PubTemplateKeywordResponse = serde_json::from_str(json).unwrap();
625 assert_eq!(response.data.len(), 1);
626 assert_eq!(response.data[0].kid, 1);
627 }
628
629 #[test]
630 fn test_user_notify_response_parse() {
631 let json = r#"{"errcode": 0, "errmsg": "ok", "status": "success"}"#;
632
633 let response: UserNotifyResponse = serde_json::from_str(json).unwrap();
634 assert_eq!(response.errcode, 0);
635 assert_eq!(response.extra.get("status").unwrap(), "success");
636 }
637
638 #[tokio::test]
639 async fn test_send_success() {
640 use wiremock::matchers::{body_json, method, path, query_param};
641 use wiremock::{Mock, MockServer, ResponseTemplate};
642
643 let mock_server = MockServer::start().await;
644
645 Mock::given(method("POST"))
646 .and(path("/cgi-bin/message/subscribe/send"))
647 .and(query_param("access_token", "test_token"))
648 .and(body_json(serde_json::json!({
649 "touser": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M",
650 "template_id": "template_id_456",
651 "data": {
652 "thing1": {"value": "Order #123"}
653 }
654 })))
655 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
656 "errcode": 0,
657 "errmsg": "ok"
658 })))
659 .mount(&mock_server)
660 .await;
661
662 Mock::given(method("GET"))
663 .and(path("/cgi-bin/token"))
664 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
665 "access_token": "test_token",
666 "expires_in": 7200,
667 "errcode": 0,
668 "errmsg": ""
669 })))
670 .mount(&mock_server)
671 .await;
672
673 let context = create_test_context(&mock_server.uri());
674 let subscribe_api = SubscribeApi::new(context);
675
676 let mut data = SubscribeMessageData::new();
677 data.insert(
678 "thing1".to_string(),
679 SubscribeMessageValue::new("Order #123"),
680 );
681
682 let options = SubscribeMessageOptions {
683 touser: OpenId::new("o6_bmjrPTlm6_2sgVt7hMZOPfL2M").unwrap(),
684 template_id: "template_id_456".to_string(),
685 data,
686 page: None,
687 miniprogram_state: None,
688 lang: None,
689 };
690
691 let result = subscribe_api.send(options).await;
692 assert!(result.is_ok());
693 }
694}