1use crate::{contact::UserInfo, DingTalk};
2use deadpool_redis::redis::cmd;
3use deadpool_redis::Pool;
4
5use log::{error, info, warn};
6use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::Arc;
10
11#[derive(Serialize, Deserialize, Debug)]
12pub struct Organization {
13 #[serde(rename = "licenseUrl")]
14 pub license_url: String,
15
16 #[serde(rename = "orgName")]
17 pub name: String,
18
19 #[serde(rename = "registrationNum")]
20 pub registration_no: String,
21
22 #[serde(rename = "unifiedSocialCredit")]
23 pub unified_social_credit: String,
24
25 #[serde(rename = "organizationCode")]
26 pub organization_code: String,
27
28 #[serde(rename = "legalPerson")]
29 pub legal_person: String,
30
31 #[serde(rename = "licenseOrgName")]
32 pub license_org_name: String,
33
34 #[serde(rename = "authLevel")]
35 pub auth_level: i32,
36}
37
38impl DingTalk {
39 pub fn set_corp_id(&self, corp_id: String) -> OrgApp {
53 OrgApp::new(
54 self.appid.clone(),
55 self.app_secret.clone(),
56 corp_id,
57 self.rdb.clone(),
58 )
59 }
60}
61
62#[derive(Serialize, Deserialize, Debug)]
63pub struct UserGetByCodeResponse {
64 device_id: String,
65 #[serde(rename = "name")]
66 pub username: String,
67 #[serde(rename = "sys")]
68 is_admin: bool,
69 #[serde(rename = "sys_level")]
70 level: i32, #[serde(rename = "unionid")]
72 pub union_id: String,
73 #[serde(rename = "userid")]
74 pub user_id: String,
75}
76
77#[derive(Serialize, Deserialize, Debug)]
78pub struct Department {
79 #[serde(rename = "dept_id")]
80 pub id: i32,
81 #[serde(rename = "order")]
82 pub sort_id: i64,
83}
84
85#[derive(Serialize, Deserialize, Debug)]
86pub struct LeaderInDepartment {
87 #[serde(rename = "dept_id")]
88 pub id: i32,
89 pub leader: bool,
90}
91
92#[derive(Serialize, Deserialize, Debug)]
93pub struct Role {
94 pub id: i32,
95 pub name: String,
96 pub group_name: String,
97}
98
99#[derive(Serialize, Deserialize, Debug)]
100pub struct UserGetProfileResponse {
101 pub active: bool,
102 pub admin: bool,
103 pub avatar: String,
104 pub boss: bool,
105 pub create_time: String,
106 pub dept_id_list: Vec<i32>,
107 pub dept_order_list: Vec<Department>,
108 #[serde(default)]
109 pub email: Option<String>,
110 pub exclusive_account: bool,
111 pub hide_mobile: bool,
112 #[serde(default)]
113 pub job_number: String,
114 pub leader_in_dept: Vec<LeaderInDepartment>,
115 pub mobile: String,
116 #[serde(rename = "name")]
117 pub username: String,
118 #[serde(default)]
119 pub org_email: Option<String>,
120 pub real_authed: bool,
121 pub remark: String,
122 pub role_list: Vec<Role>,
123 pub senior: bool,
124 pub state_code: String,
125 pub telephone: String,
126 #[serde(default)]
127 pub title: String,
128 #[serde(default)]
129 pub union_emp_ext: HashMap<String, String>,
130 #[serde(rename = "unionid")]
131 pub union_id: String,
132 #[serde(rename = "userid")]
133 pub user_id: String,
134 pub work_place: String,
135}
136
137pub struct OrgApp {
138 appid: String,
139 app_secret: String,
140 corp_id: String,
141 client: reqwest::Client,
142 rdb: Arc<Pool>,
143}
144
145impl OrgApp {
146 pub fn new(appid: String, app_secret: String, corp_id: String, rdb: Arc<Pool>) -> OrgApp {
147 OrgApp {
148 appid,
149 app_secret,
150 corp_id,
151 rdb,
152 client: reqwest::Client::new(),
153 }
154 }
155
156 async fn get_access_token(&self) -> Result<String, Box<dyn std::error::Error>> {
157 #[derive(Serialize, Deserialize, Debug)]
158 struct AccessToken {
159 access_token: String,
160 #[serde(rename = "expires_in")]
161 expire_in: i32,
162 }
163
164 let mut rdb = self.rdb.get().await.unwrap();
165 let value: Option<String> = cmd("GET")
166 .arg(&self.corp_id)
167 .query_async(&mut rdb)
168 .await
169 .unwrap_or(None);
170
171 if let Some(bytes) = value {
172 return Ok(bytes);
173 }
174
175 let mut params = HashMap::new();
176 params.insert("client_id", self.appid.clone());
177 params.insert("client_secret", self.app_secret.clone());
178 params.insert("grant_type", "client_credentials".to_string());
179
180 let response = self
181 .client
182 .post(format!(
183 "https://api.dingtalk.com/v1.0/oauth2/{}/token",
184 self.corp_id
185 ))
186 .json(¶ms)
187 .send()
188 .await?;
189
190 if !response.status().is_success() {
191 return Err(format!(
192 "Failed to get organization access token: {}",
193 response.status()
194 )
195 .into());
196 }
197
198 let result = response.json::<AccessToken>().await?;
199 warn!("fetch_org_access_token result: {:#?}", result);
200
201 let mut rdb = self.rdb.get().await.unwrap();
202 cmd("SETEX")
203 .arg(&self.corp_id)
204 .arg(7200)
205 .arg(&result.access_token)
206 .query_async::<()>(&mut rdb)
207 .await
208 .unwrap();
209
210 Ok(result.access_token)
211 }
212
213 pub async fn get_organization(&self) -> Result<Organization, Box<dyn std::error::Error>> {
229 let mut headers = HeaderMap::new();
230 match self.get_access_token().await {
231 Ok(at) => {
232 headers.insert(
233 HeaderName::from_static("x-acs-dingtalk-access-token"),
234 HeaderValue::from_str(&at).unwrap(),
235 );
236 }
237 Err(e) => return Err(e),
238 };
239
240 let url: String = format!(
241 "https://api.dingtalk.com/v1.0/contact/organizations/authInfos?targetCorpId={}",
242 self.corp_id
243 );
244 let response = self.client.get(&url).headers(headers).send().await?;
245
246 if !response.status().is_success() {
247 return Err(format!("Failed to get organization: {}", response.status()).into());
248 }
249
250 let result = response.json::<Organization>().await?;
251 info!("get_organization: {:?}", result);
252
253 Ok(result)
254 }
255
256 async fn get_user_id(&self, code: String) -> Result<String, Box<dyn std::error::Error>> {
275 let token = match self.get_access_token().await {
276 Ok(value) => value,
277 Err(e) => return Err(e),
278 };
279
280 let mut params = HashMap::new();
281 params.insert("code", code);
282
283 let response = self
284 .client
285 .post(format!(
286 "https://oapi.dingtalk.com/topapi/v2/user/getuserinfo?access_token={}",
287 token
288 ))
289 .json(¶ms)
290 .send()
291 .await?;
292
293 if !response.status().is_success() {
294 return Err(format!("Failed to response user info: {}", response.status()).into());
295 }
296
297 #[derive(Serialize, Deserialize, Debug)]
298 struct Response {
299 errcode: i32,
300 errmsg: String,
301 result: UserGetByCodeResponse,
302 request_id: Option<String>,
303 }
304 let user = match response.json::<Response>().await {
305 Ok(value) => value.result,
306 Err(e) => {
307 error!("response get_user info {:?}", e);
308 return Err(e.into());
309 }
310 };
311
312 info!("get_org_user_id {:?}", &user);
313
314 Ok(user.user_id)
315 }
316
317 pub async fn get_userinfo(&self, code: String) -> Result<UserInfo, Box<dyn std::error::Error>> {
337 let mut params = HashMap::new();
338 match self.get_user_id(code.clone()).await {
339 Ok(id) => params.insert("userid", id),
340 Err(e) => return Err(e),
341 };
342
343 let at = match self.get_access_token().await {
344 Ok(at) => at,
345 Err(e) => return Err(e),
346 };
347
348 let response = self
349 .client
350 .post(format!(
351 "https://oapi.dingtalk.com/topapi/v2/user/get?access_token={}",
352 at
353 ))
354 .json(¶ms)
355 .send()
356 .await?;
357
358 if !response.status().is_success() {
359 return Err(format!(
360 "Failed to response get org user info: {}",
361 response.status()
362 )
363 .into());
364 }
365
366 #[derive(Serialize, Deserialize, Debug)]
367 struct Response {
368 errcode: i32,
369 errmsg: String,
370 result: UserGetProfileResponse,
371 request_id: Option<String>,
372 }
373 let profile = match response.json::<Response>().await {
374 Ok(res) => res.result,
375 Err(e) => {
376 error!("response get org user info {:?}", e);
377 return Err(e.into());
378 }
379 };
380 info!("get org user info {:?}", &profile);
381
382 let profile: UserInfo = UserInfo {
383 email: profile.org_email.clone(),
384 union_id: profile.union_id.clone(),
385 username: profile.username.clone(),
386 visitor: None,
387 mobile: Some(profile.mobile.clone()),
388 open_id: None,
389 state_code: "".to_string(),
390 };
391
392 Ok(profile)
393 }
394
395 pub async fn get_employee_count(
409 &self,
410 only_active: Option<bool>,
411 ) -> Result<i32, Box<dyn std::error::Error>> {
412 let mut params = HashMap::new();
413 params.insert("only_active", only_active.unwrap_or(false));
414
415 let at = match self.get_access_token().await {
416 Ok(at) => at,
417 Err(e) => return Err(e),
418 };
419
420 let response = self
421 .client
422 .post(format!(
423 "https://oapi.dingtalk.com/topapi/user/count?access_token={}",
424 at
425 ))
426 .json(¶ms)
427 .send()
428 .await?;
429
430 if !response.status().is_success() {
431 return Err(format!(
432 "Failed to response get employee count: {}",
433 response.status()
434 )
435 .into());
436 }
437
438 #[derive(Serialize, Deserialize, Debug)]
439 struct Response {
440 errcode: i32,
441 errmsg: String,
442 result: CountUserResponse,
443 request_id: Option<String>,
444 }
445
446 let res = response.json::<Response>().await?;
447
448 Ok(res.result.count)
449 }
450
451 pub async fn query_on_job_employees(
465 &self,
466 status: String,
467 offset: i32,
468 ) -> Result<PageResult, Box<dyn std::error::Error>> {
469 let mut params: HashMap<&str, String> = HashMap::new();
470 params.insert("status_list", status);
471 params.insert("offset", format!("{}", offset));
472 params.insert("size", "50".to_string());
473
474 let at = match self.get_access_token().await {
475 Ok(at) => at,
476 Err(e) => return Err(e),
477 };
478
479 let response = self
480 .client
481 .post(format!(
482 "https://oapi.dingtalk.com/topapi/smartwork/hrm/employee/queryonjob?access_token={}",
483 at
484 ))
485 .json(¶ms)
486 .send()
487 .await?;
488
489 if !response.status().is_success() {
490 return Err(format!(
491 "Failed to response get employee count: {}",
492 response.status()
493 )
494 .into());
495 }
496
497 #[derive(Serialize, Deserialize, Debug)]
498 struct Response {
499 errcode: i32,
500 errmsg: String,
501 result: PageResult,
502 request_id: Option<String>,
503 }
504
505 let res = response.json::<Response>().await?;
506
507 Ok(res.result)
508 }
509
510 pub async fn query_off_job_employees(
530 &self,
531 offset: i64,
532 ) -> Result<PageResult, Box<dyn std::error::Error>> {
533 let mut headers = HeaderMap::new();
534 match self.get_access_token().await {
535 Ok(at) => headers.insert(
536 HeaderName::from_static("x-acs-dingtalk-access-token"),
537 HeaderValue::from_str(&at).unwrap(),
538 ),
539 Err(e) => return Err(e),
540 };
541
542 let url: String = format!(
543 "https://api.dingtalk.com/v1.0/hrm/employees/dismissions?nextToken={}&maxResults=50",
544 offset
545 );
546 info!("query_off_job_employees: {}", url);
547
548 let response = self.client.get(&url).headers(headers).send().await?;
549 if !response.status().is_success() {
550 return Err(format!("Failed to get user info: {}", response.status()).into());
551 }
552
553 #[derive(Serialize, Deserialize, Debug)]
554 struct Response {
555 #[serde(rename = "nextToken")]
556 next_cursor: i64,
557 #[serde(rename = "hasMore")]
558 has_more: bool,
559 #[serde(rename = "userIdList")]
560 data: Vec<String>,
561 }
562 let result = response.json::<Response>().await?;
563 info!("query_off_job_employees: {:?}", &result);
564
565 let reply = PageResult {
566 data: result.data,
567 next_cursor: Some(result.next_cursor),
568 };
569
570 Ok(reply)
571 }
572
573 pub async fn get_employee_userinfo(
595 &self,
596 user_id: String,
597 ) -> Result<EmployeeUser, Box<dyn std::error::Error>> {
598 let mut params: HashMap<&str, String> = HashMap::new();
599 params.insert("language", "zh_CN".to_string());
600 params.insert("userid", user_id);
601
602 let at = match self.get_access_token().await {
603 Ok(at) => at,
604 Err(e) => return Err(e),
605 };
606
607 let response = self
608 .client
609 .post(format!(
610 "https://oapi.dingtalk.com/topapi/v2/user/get?access_token={}",
611 at
612 ))
613 .json(¶ms)
614 .send()
615 .await?;
616
617 if !response.status().is_success() {
618 return Err(format!(
619 "Failed to response get employee count: {}",
620 response.status()
621 )
622 .into());
623 }
624
625 #[derive(Serialize, Deserialize, Debug)]
626 struct Response {
627 errcode: i32,
628 errmsg: String,
629 result: EmployeeUser,
630 request_id: Option<String>,
631 }
632
633 let result = match response.json::<Response>().await {
634 Ok(res) => res.result,
635 Err(e) => {
636 error!("Failed to get user info: {}", e);
637 return Err(e.into());
638 }
639 };
640
641 Ok(result)
642 }
643}
644
645#[derive(Serialize, Deserialize, Debug)]
646struct CountUserResponse {
647 count: i32,
648}
649
650#[derive(Serialize, Deserialize, Debug)]
651pub struct PageResult {
652 #[serde(rename = "data_list")]
653 pub data: Vec<String>,
654 pub next_cursor: Option<i64>,
655}
656
657#[derive(Serialize, Deserialize, Debug)]
658pub struct EmployeeUser {
659 #[serde(rename = "unionid")]
660 pub union_id: String,
661 #[serde(rename = "userid")]
662 pub user_id: String,
663 #[serde(rename = "name")]
664 pub username: String,
665 #[serde(rename = "avatar")]
666 pub profile_url: String,
667 pub state_code: String,
668
669 #[serde(default)]
670 pub manager_userid: Option<String>,
671
672 pub mobile: String,
673 pub hide_mobile: bool,
674 pub telephone: String,
675
676 #[serde(default)]
677 pub job_number: String,
678
679 #[serde(default)]
680 pub title: String,
681
682 #[serde(default)]
683 pub email: Option<String>,
684 pub work_place: String,
685 pub remark: String,
686 pub exclusive_account: bool,
687
688 #[serde(default)]
689 pub org_email: Option<String>,
690
691 pub dept_id_list: Vec<i32>,
692 pub dept_order_list: Vec<Department>,
693
694 #[serde(default)]
695 pub extension: Option<String>,
696
697 #[serde(default)]
698 pub hired_date: Option<u64>,
699
700 pub active: bool,
701 pub real_authed: bool,
702 pub senior: bool,
703 pub admin: bool,
704 pub boss: bool,
705 pub leader_in_dept: Option<Vec<LeaderInDepartment>>,
706
707 #[serde(default)]
708 pub role_list: Option<Vec<Role>>,
709 #[serde(default)]
710 pub union_emp_ext: HashMap<String, String>,
711}