async_wechat/official_account/
qrcode.rs

1use std::{any::TypeId, str::FromStr};
2
3use crate::OfficialAccount;
4
5use serde::{Deserialize, Serialize};
6use urlencoding::encode;
7
8pub(crate) const QR_CREATE_URL: &str =
9    "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=";
10pub(crate) const QR_IMG_URL: &str = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=";
11
12#[cfg(test)]
13mod tests {
14    use uuid::Uuid;
15
16    use crate::{
17        Config, OfficialAccount,
18        official_account::qrcode::{self, TicketRequest},
19    };
20    use std::env;
21
22    #[tokio::test]
23    #[ignore]
24    async fn get_qr_ticket() {
25        dotenv::dotenv().ok();
26
27        let appid = env::var("APPID").expect("APPID not set");
28        let app_secret = env::var("APP_SECRET").expect("APP_SECRET not set");
29        let redis_url = env::var("REDIS_URL").expect("REDIS_URL not set");
30
31        let config = Config {
32            appid: appid.clone(),
33            app_secret: app_secret.clone(),
34            token: "wechat".to_string(),
35            encoding_aes_key: None,
36        };
37        let account = OfficialAccount::new(config, redis_url);
38
39        let key = format!("login:{}", Uuid::new_v4().to_string());
40
41        let scene = qrcode::TicketScene {
42            scene_id: None,
43            scene_str: Some(key.clone()),
44        };
45        let params = TicketRequest {
46            expire_seconds: 200000,
47            action_name: qrcode::TicketActionName::QrStrScene,
48            action_info: qrcode::TicketActionInfo { scene },
49        };
50
51        let at = account.get_qr_ticket(params).await;
52        println!("get_qr_ticket: {:#?}", at.unwrap().show_qrcode());
53    }
54}
55
56#[derive(Debug, Serialize, Deserialize)]
57#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
58pub enum TicketActionName {
59    QrScene, // => QR_SCENE
60    QrStrScene,
61    QrLimitScene,
62    QrLimitStrScene,
63}
64
65#[derive(Serialize, Deserialize, Debug)]
66pub struct TicketActionInfo {
67    pub scene: TicketScene,
68}
69
70#[derive(Serialize, Deserialize, Debug)]
71pub struct TicketScene {
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub scene_id: Option<u32>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub scene_str: Option<String>,
76}
77
78#[derive(Serialize, Deserialize, Debug)]
79pub struct TicketRequest {
80    pub expire_seconds: u64,
81    pub action_name: TicketActionName,
82    pub action_info: TicketActionInfo,
83}
84
85#[derive(Serialize, Deserialize, Debug)]
86pub struct TicketResponse {
87    pub ticket: String,
88    pub expire_seconds: u64,
89    pub url: String,
90}
91
92pub fn build_tmp_qr_request<T>(exp: u64, scene: T) -> TicketRequest
93where
94    T: 'static + ToString + FromStr,
95    <T as FromStr>::Err: std::fmt::Debug,
96{
97    let type_id = TypeId::of::<T>();
98    let mut ticket_scene = TicketScene {
99        scene_id: None,
100        scene_str: None,
101    };
102
103    let action_name;
104
105    if type_id == TypeId::of::<String>() || type_id == TypeId::of::<&str>() {
106        action_name = TicketActionName::QrStrScene;
107        ticket_scene.scene_str = Some(scene.to_string());
108    } else {
109        action_name = TicketActionName::QrScene;
110        let parsed_id = scene
111            .to_string()
112            .parse::<u32>()
113            .expect("scene parse failed");
114        ticket_scene.scene_id = Some(parsed_id);
115    }
116
117    let expire_seconds = exp.clamp(60, 2592000);
118
119    TicketRequest {
120        expire_seconds,
121        action_name,
122        action_info: TicketActionInfo {
123            scene: ticket_scene,
124        },
125    }
126}
127
128impl TicketResponse {
129    /// Generates a URL to display the QR code using the ticket.
130    ///
131    /// This function encodes the ticket and constructs a URL that can be used to
132    /// view the QR code associated with the provided ticket.
133    ///
134    /// # Returns
135    ///
136    /// * A `String` representing the URL to display the QR code.
137    pub fn show_qrcode(&self) -> String {
138        let ticket = encode(&self.ticket);
139        format!("{}{}", QR_IMG_URL, ticket)
140    }
141}
142
143impl OfficialAccount {
144    /// [Create a QR code ticket for the official account](https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html)
145    ///
146    /// This function creates a QR code ticket for the official account and returns
147    /// a string representing the ticket.
148    ///
149    /// # Errors
150    ///
151    /// * Returns an error if the HTTP request fails or returns a non-success status,
152    ///   or if the response cannot be deserialized into a `String`.
153    pub async fn get_qr_ticket(
154        &self,
155        req: TicketRequest,
156    ) -> Result<TicketResponse, Box<dyn std::error::Error>> {
157        println!("req: {:#?}", &req);
158        let token = self.token().await?;
159
160        let url = format!("{}{}", QR_CREATE_URL, token);
161        let response = match self.client.post(url).json(&req).send().await {
162            Ok(r) => r,
163            Err(e) => {
164                println!("json err: {:#?}", e);
165                return Err(e.into());
166            }
167        };
168
169        let result = match response.json::<TicketResponse>().await {
170            Ok(r) => r,
171            Err(e) => {
172                println!("json err: {:#?}", e);
173                return Err(e.into());
174            }
175        };
176
177        Ok(result)
178    }
179}