1use std::collections::HashMap;
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use tracing::{event, Level};
6
7use crate::{
8 credential::{GenericAccessToken, GetAccessToken, GetStableAccessToken, StableAccessToken},
9 error::Error::InternalServer,
10 response::Response,
11 Result,
12};
13
14#[derive(Debug, Serialize, Deserialize, Clone)]
15pub struct User {
16 nickname: String,
17 gender: u8,
18 country: String,
19 province: String,
20 city: String,
21 avatar: String,
22 watermark: Watermark,
23}
24
25impl User {
26 pub fn nickname(&self) -> &str {
27 &self.nickname
28 }
29
30 pub fn gender(&self) -> u8 {
31 self.gender
32 }
33
34 pub fn country(&self) -> &str {
35 &self.country
36 }
37
38 pub fn province(&self) -> &str {
39 &self.province
40 }
41
42 pub fn city(&self) -> &str {
43 &self.city
44 }
45
46 pub fn avatar(&self) -> &str {
47 &self.avatar
48 }
49
50 pub fn app_id(&self) -> &str {
51 &self.watermark.app_id
52 }
53
54 pub fn timestamp(&self) -> u64 {
55 self.watermark.timestamp
56 }
57}
58
59#[derive(Debug, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub(crate) struct UserBuilder {
62 #[serde(rename = "nickName")]
63 nickname: String,
64 gender: u8,
65 country: String,
66 province: String,
67 city: String,
68 #[serde(rename = "avatarUrl")]
69 avatar: String,
70 watermark: WatermarkBuilder,
71}
72
73impl UserBuilder {
74 pub(crate) fn build(self) -> User {
75 User {
76 nickname: self.nickname,
77 gender: self.gender,
78 country: self.country,
79 province: self.province,
80 city: self.city,
81 avatar: self.avatar,
82 watermark: self.watermark.build(),
83 }
84 }
85}
86
87#[derive(Debug, Serialize, Deserialize, Clone)]
88pub struct Contact {
89 phone_number: String,
90 pure_phone_number: String,
91 country_code: String,
92 watermark: Watermark,
93}
94
95impl Contact {
96 pub fn phone_number(&self) -> &str {
97 &self.phone_number
98 }
99
100 pub fn pure_phone_number(&self) -> &str {
101 &self.pure_phone_number
102 }
103
104 pub fn country_code(&self) -> &str {
105 &self.country_code
106 }
107
108 pub fn app_id(&self) -> &str {
109 &self.watermark.app_id
110 }
111
112 pub fn timestamp(&self) -> u64 {
113 self.watermark.timestamp
114 }
115}
116
117#[derive(Debug, Deserialize, Clone)]
118pub(crate) struct ContactBuilder {
119 #[serde(rename = "phone_info")]
120 inner: PhoneInner,
121}
122
123impl ContactBuilder {
124 pub(crate) fn build(self) -> Contact {
125 Contact {
126 phone_number: self.inner.phone_number,
127 pure_phone_number: self.inner.pure_phone_number,
128 country_code: self.inner.country_code,
129 watermark: self.inner.watermark.build(),
130 }
131 }
132}
133
134#[derive(Debug, Deserialize, Clone)]
135#[serde(rename_all = "camelCase")]
136struct PhoneInner {
137 #[serde(rename = "phoneNumber")]
138 phone_number: String,
139 #[serde(rename = "purePhoneNumber")]
140 pure_phone_number: String,
141 country_code: String,
142 watermark: WatermarkBuilder,
143}
144
145#[derive(Debug, Serialize, Deserialize, Clone)]
146struct Watermark {
147 app_id: String,
148 timestamp: u64,
149}
150
151#[derive(Debug, Deserialize, Clone)]
152struct WatermarkBuilder {
153 #[serde(rename = "appid")]
154 app_id: String,
155 timestamp: u64,
156}
157
158impl WatermarkBuilder {
159 fn build(self) -> Watermark {
160 Watermark {
161 app_id: self.app_id,
162 timestamp: self.timestamp,
163 }
164 }
165}
166
167#[async_trait]
168pub trait GetContact {
169 const PHONE: &str = "https://api.weixin.qq.com/wxa/business/getuserphonenumber";
170
171 async fn get_contact(&self, code: &str, open_id: Option<&str>) -> Result<Contact>;
172}
173
174#[async_trait]
175impl GetContact for GenericAccessToken {
176 async fn get_contact(&self, code: &str, open_id: Option<&str>) -> Result<Contact> {
177 use reqwest::Client;
178
179 event!(Level::DEBUG, "code: {}, open_id: {:?}", code, open_id);
180
181 let mut query = HashMap::new();
182 let mut body = HashMap::new();
183
184 query.insert("access_token", self.access_token().await?);
185 body.insert("code", code);
186
187 if let Some(open_id) = open_id {
188 body.insert("openid", open_id);
189 }
190
191 let response = Client::new()
192 .post(Self::PHONE)
193 .query(&query)
194 .json(&body)
195 .send()
196 .await?;
197
198 event!(Level::DEBUG, "response: {:#?}", response);
199
200 if response.status().is_success() {
201 let response = response.json::<Response<ContactBuilder>>().await?;
202
203 let builder = response.extract()?;
204
205 event!(Level::DEBUG, "contact builder: {:#?}", builder);
206
207 Ok(builder.build())
208 } else {
209 Err(InternalServer(response.text().await?))
210 }
211 }
212}
213
214#[async_trait]
215impl GetContact for GenericAccessToken<StableAccessToken> {
216 async fn get_contact(&self, code: &str, open_id: Option<&str>) -> Result<Contact> {
217 use reqwest::Client;
218
219 event!(Level::DEBUG, "code: {}, open_id: {:?}", code, open_id);
220
221 let mut query = HashMap::new();
222 let mut body = HashMap::new();
223
224 query.insert("access_token", self.access_token().await?);
225 body.insert("code", code);
226
227 if let Some(open_id) = open_id {
228 body.insert("openid", open_id);
229 }
230
231 let response = Client::new()
232 .post(Self::PHONE)
233 .query(&query)
234 .json(&body)
235 .send()
236 .await?;
237
238 event!(Level::DEBUG, "response: {:#?}", response);
239
240 if response.status().is_success() {
241 let response = response.json::<Response<ContactBuilder>>().await?;
242
243 let builder = response.extract()?;
244
245 event!(Level::DEBUG, "contact builder: {:#?}", builder);
246
247 Ok(builder.build())
248 } else {
249 Err(InternalServer(response.text().await?))
250 }
251 }
252}