async_dingtalk/
organization.rs

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    /// Creates a new `OrgApp` instance with the given corporate ID and configuration.
40    ///
41    /// This method creates a new instance of `OrgApp` with the given corporate ID and the same
42    /// configuration as the current `DingTalk` instance. This is useful for accessing the DingTalk
43    /// API of a specific organization.
44    ///
45    /// # Arguments
46    ///
47    /// * `corp_id` - The corporate ID of the organization to create the `OrgApp` for.
48    ///
49    /// # Returns
50    ///
51    /// A new `OrgApp` instance with the given corporate ID and configuration.
52    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, //1: 主管理员 2:子管理员 100:老板 0:其他
71    #[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(&params)
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    /// Retrieves the organization information associated with the provided corporate ID.
214    ///
215    /// [Documents](https://open.dingtalk.com/document/orgapp/obtain-enterprise-authentication-information)
216    ///
217    /// This function first obtains an access token for the organization, then makes a request to the
218    /// DingTalk API to retrieve the organization details.
219    ///
220    /// # Arguments
221    ///
222    /// * `&self` - The `OrgApp` instance to use for the request.
223    ///
224    /// # Returns
225    ///
226    /// A `Result` containing an `Organization` struct with the organization details if successful,
227    /// otherwise an error string.
228    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    /// Retrieves the user ID associated with the given authorization code.
257    ///
258    /// This asynchronous function sends a POST request to the DingTalk API to fetch
259    /// user details associated with the provided `code`. The request includes an
260    /// access token in the query parameters for authentication.
261    ///
262    /// # Arguments
263    ///
264    /// * `code` - A string representing the authorization code to fetch user information.
265    ///
266    /// # Returns
267    ///
268    /// Returns a `Result` containing the user ID as a `String` if the request is successful,
269    /// or an error if the request fails or if the response status is not successful.
270    ///
271    /// # Errors
272    ///
273    /// Returns an error if the response status is not successful, or if the request fails.
274    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(&params)
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    /// Retrieves user information from DingTalk using the provided code.
318    ///
319    /// [Documents](https://open.dingtalk.com/document/orgapp/get-user-info-by-code)
320    ///
321    /// This asynchronous function sends a POST request to the DingTalk API to fetch
322    /// user details associated with the provided `code`. The request includes an
323    /// access token in the query parameters for authentication.
324    ///
325    /// # Arguments
326    ///
327    /// * `code` - A string representing the authorization code to fetch user information.
328    ///
329    /// # Returns
330    ///
331    /// Returns a `Result` containing a `UserInfo` object if the request is successful,
332    /// or an error if the request fails or if the response status is not successful.
333    ///
334    /// # Errors
335    ///
336    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(&params)
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    /// Retrieves the total number of employees in the organization.
396    ///
397    /// [获取员工人数](https://open.dingtalk.com/document/orgapp/obtain-the-number-of-employees-v2)
398    ///
399    /// If `only_active` is `Some(true)`, only active employees are counted.
400    ///
401    /// # Arguments
402    ///
403    /// * `only_active` - An optional boolean indicating whether to only count active employees.
404    ///
405    /// # Returns
406    ///
407    /// A `Result` containing the total number of employees if successful, otherwise an error string.
408    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(&params)
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    /// Query employees on job.
452    ///
453    /// [获取在职员工列表](https://open.dingtalk.com/document/orgapp/intelligent-personnel-query-the-list-of-on-the-job-employees-of-the)
454    ///
455    /// # Arguments
456    ///
457    /// * `status` - A string array representing the status of the employees to query.
458    /// * `offset` - An integer representing the offset of the query.
459    ///
460    /// # Returns
461    ///
462    /// A `Result` containing a `PageResult` object if the request is successful,
463    /// or an error if the request fails or if the response status is not successful.
464    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(&params)
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    /// Retrieves a list of employees who are no longer on the job.
511    ///
512    /// [获取离职员工列表](https://open.dingtalk.com/document/orgapp/obtain-the-list-of-employees-who-have-left)
513    ///
514    /// The results are paginated, with the `offset` parameter specifying the starting
515    /// index of the page. The `nextToken` parameter is used to fetch the next page.
516    ///
517    /// # Arguments
518    ///
519    /// * `offset` - The starting index of the page to fetch.
520    ///
521    /// # Returns
522    ///
523    /// A `Result` containing a `PageResult` object if the request is successful,
524    /// or an error if the request fails or if the response status is not successful.
525    ///
526    /// # Errors
527    ///
528    /// Returns an error if the response status is not successful, or if the request fails.
529    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    /// Retrieves detailed profile information of an employee using their user ID.
574    ///
575    /// [查询用户详情](https://open.dingtalk.com/document/orgapp/query-user-details)
576    ///
577    /// This asynchronous function sends a POST request to the DingTalk API to fetch
578    /// detailed profile information of an employee based on the provided `user_id`.
579    /// The request includes an access token in the query parameters for authentication
580    /// and specifies the response language.
581    ///
582    /// # Arguments
583    ///
584    /// * `user_id` - A string representing the unique identifier of the employee.
585    ///
586    /// # Returns
587    ///
588    /// Returns a `Result` containing a `UserGetProfileResponse` object if the request is successful,
589    /// or an error if the request fails or if the response status is not successful.
590    ///
591    /// # Errors
592    ///
593    /// Returns an error if the response status is not successful, or if the request fails.
594    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(&params)
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}