Skip to main content

dingtalk_sdk/api/
async_enterprise.rs

1use serde::de::DeserializeOwned;
2
3use super::is_private_conversation;
4
5use crate::{
6    auth::AppCredentials,
7    client::async_client::Client,
8    error::{Error, Result},
9    transport::{
10        DEFAULT_MSG_KEY, parse_approval_create_response, parse_approval_get_response,
11        parse_get_token_response, parse_standard_api_text_response, parse_topapi_result_response,
12        parse_topapi_unit_response,
13    },
14    types::{
15        ApprovalCreateProcessInstanceRequest, ApprovalListProcessInstanceIdsRequest,
16        ApprovalListProcessInstanceIdsResult, ApprovalProcessInstance,
17        ApprovalTerminateProcessInstanceRequest, ContactCreateDepartmentRequest,
18        ContactCreateDepartmentResult, ContactCreateUserRequest, ContactCreateUserResult,
19        ContactDeleteDepartmentRequest, ContactDeleteUserRequest, ContactDepartment,
20        ContactGetDepartmentRequest, ContactGetUserByMobileRequest, ContactGetUserByUnionIdRequest,
21        ContactGetUserRequest, ContactListSubDepartmentIdsRequest,
22        ContactListSubDepartmentIdsResult, ContactListSubDepartmentsRequest,
23        ContactListSubDepartmentsResult, ContactListUsersRequest, ContactListUsersResult,
24        ContactUpdateDepartmentRequest, ContactUpdateUserRequest, ContactUser,
25        internal::{GroupMessageRequest, MsgParam, OtoMessageRequest},
26    },
27};
28
29/// Async enterprise robot service.
30#[derive(Clone)]
31pub struct EnterpriseService {
32    client: Client,
33    credentials: AppCredentials,
34    robot_code: String,
35}
36
37impl EnterpriseService {
38    pub(crate) fn new(
39        client: Client,
40        appkey: impl Into<String>,
41        appsecret: impl Into<String>,
42        robot_code: impl Into<String>,
43    ) -> Self {
44        Self {
45            client,
46            credentials: AppCredentials::new(appkey, appsecret),
47            robot_code: robot_code.into(),
48        }
49    }
50
51    /// Retrieves enterprise access token and refreshes cache when needed.
52    pub async fn get_access_token(&self) -> Result<String> {
53        if let Some(token) = self.client.cached_access_token(&self.credentials) {
54            return Ok(token);
55        }
56
57        let endpoint = self.client.webhook_endpoint(&["gettoken"])?;
58        let payload = parse_get_token_response(
59            self.client
60                .webhook_http()
61                .get(endpoint.as_str())
62                .query_pair("appkey", self.credentials.appkey().to_string())
63                .query_pair("appsecret", self.credentials.appsecret().to_string())
64                .send_response()
65                .await?,
66            self.client.body_snippet(),
67        )?;
68        let access_token = payload.token;
69
70        self.client
71            .store_access_token(&self.credentials, access_token.clone(), payload.expires_in);
72
73        Ok(access_token)
74    }
75
76    async fn post_topapi_result<T, B>(&self, segments: &[&str], body: &B) -> Result<T>
77    where
78        T: DeserializeOwned,
79        B: serde::Serialize + ?Sized,
80    {
81        let access_token = self.get_access_token().await?;
82        let endpoint = self.client.webhook_endpoint(segments)?;
83        parse_topapi_result_response(
84            self.client
85                .webhook_http()
86                .post(endpoint.as_str())
87                .query_pair("access_token", access_token)
88                .json(body)?
89                .send_response()
90                .await?,
91            self.client.body_snippet(),
92        )
93    }
94
95    async fn post_topapi_unit<B>(&self, segments: &[&str], body: &B) -> Result<()>
96    where
97        B: serde::Serialize + ?Sized,
98    {
99        let access_token = self.get_access_token().await?;
100        let endpoint = self.client.webhook_endpoint(segments)?;
101        parse_topapi_unit_response(
102            self.client
103                .webhook_http()
104                .post(endpoint.as_str())
105                .query_pair("access_token", access_token)
106                .json(body)?
107                .send_response()
108                .await?,
109            self.client.body_snippet(),
110        )
111    }
112
113    async fn send_enterprise_message<T: serde::Serialize + ?Sized>(
114        &self,
115        segments: &[&str],
116        payload: &T,
117    ) -> Result<String> {
118        let access_token = self.get_access_token().await?;
119        let endpoint = self.client.enterprise_endpoint(segments)?;
120
121        parse_standard_api_text_response(
122            self.client
123                .enterprise_http()
124                .post(endpoint.as_str())
125                .try_header("x-acs-dingtalk-access-token", &access_token)?
126                .json(payload)?
127                .send_response()
128                .await?,
129            self.client.body_snippet(),
130        )
131    }
132
133    /// Sends a group message to a conversation.
134    pub async fn send_group_message(
135        &self,
136        open_conversation_id: &str,
137        title: &str,
138        text: &str,
139    ) -> Result<String> {
140        let request = GroupMessageRequest {
141            msg_param: MsgParam {
142                title: title.to_string(),
143                text: text.to_string(),
144            },
145            msg_key: DEFAULT_MSG_KEY,
146            robot_code: &self.robot_code,
147            open_conversation_id,
148        };
149
150        self.send_enterprise_message(&["v1.0", "robot", "groupMessages", "send"], &request)
151            .await
152    }
153
154    /// Sends a one-to-one message to a user.
155    pub async fn send_oto_message(&self, user_id: &str, title: &str, text: &str) -> Result<String> {
156        let request = OtoMessageRequest {
157            msg_param: MsgParam {
158                title: title.to_string(),
159                text: text.to_string(),
160            },
161            msg_key: DEFAULT_MSG_KEY,
162            robot_code: &self.robot_code,
163            user_ids: vec![user_id],
164        };
165
166        self.send_enterprise_message(&["v1.0", "robot", "oToMessages", "batchSend"], &request)
167            .await
168    }
169
170    /// Gets user details by user id.
171    pub async fn contact_get_user(&self, request: ContactGetUserRequest) -> Result<ContactUser> {
172        self.post_topapi_result(&["topapi", "v2", "user", "get"], &request)
173            .await
174    }
175
176    /// Gets user details by mobile.
177    pub async fn contact_get_user_by_mobile(
178        &self,
179        request: ContactGetUserByMobileRequest,
180    ) -> Result<ContactUser> {
181        self.post_topapi_result(&["topapi", "v2", "user", "getbymobile"], &request)
182            .await
183    }
184
185    /// Gets user details by union id.
186    pub async fn contact_get_user_by_unionid(
187        &self,
188        request: ContactGetUserByUnionIdRequest,
189    ) -> Result<ContactUser> {
190        self.post_topapi_result(&["topapi", "user", "getbyunionid"], &request)
191            .await
192    }
193
194    /// Lists users in a department.
195    pub async fn contact_list_users(
196        &self,
197        request: ContactListUsersRequest,
198    ) -> Result<ContactListUsersResult> {
199        self.post_topapi_result(&["topapi", "v2", "user", "list"], &request)
200            .await
201    }
202
203    /// Creates a user.
204    pub async fn contact_create_user(
205        &self,
206        request: ContactCreateUserRequest,
207    ) -> Result<ContactCreateUserResult> {
208        self.post_topapi_result(&["topapi", "v2", "user", "create"], &request)
209            .await
210    }
211
212    /// Updates a user.
213    pub async fn contact_update_user(&self, request: ContactUpdateUserRequest) -> Result<()> {
214        self.post_topapi_unit(&["topapi", "v2", "user", "update"], &request)
215            .await
216    }
217
218    /// Deletes a user.
219    pub async fn contact_delete_user(&self, request: ContactDeleteUserRequest) -> Result<()> {
220        self.post_topapi_unit(&["topapi", "v2", "user", "delete"], &request)
221            .await
222    }
223
224    /// Gets department details.
225    pub async fn contact_get_department(
226        &self,
227        request: ContactGetDepartmentRequest,
228    ) -> Result<ContactDepartment> {
229        self.post_topapi_result(&["topapi", "v2", "department", "get"], &request)
230            .await
231    }
232
233    /// Lists child departments.
234    pub async fn contact_list_sub_departments(
235        &self,
236        request: ContactListSubDepartmentsRequest,
237    ) -> Result<ContactListSubDepartmentsResult> {
238        self.post_topapi_result(&["topapi", "v2", "department", "listsub"], &request)
239            .await
240    }
241
242    /// Lists child department ids.
243    pub async fn contact_list_sub_department_ids(
244        &self,
245        request: ContactListSubDepartmentIdsRequest,
246    ) -> Result<ContactListSubDepartmentIdsResult> {
247        self.post_topapi_result(&["topapi", "v2", "department", "listsubid"], &request)
248            .await
249    }
250
251    /// Creates a department.
252    pub async fn contact_create_department(
253        &self,
254        request: ContactCreateDepartmentRequest,
255    ) -> Result<ContactCreateDepartmentResult> {
256        self.post_topapi_result(&["topapi", "v2", "department", "create"], &request)
257            .await
258    }
259
260    /// Updates a department.
261    pub async fn contact_update_department(
262        &self,
263        request: ContactUpdateDepartmentRequest,
264    ) -> Result<()> {
265        self.post_topapi_unit(&["topapi", "v2", "department", "update"], &request)
266            .await
267    }
268
269    /// Deletes a department.
270    pub async fn contact_delete_department(
271        &self,
272        request: ContactDeleteDepartmentRequest,
273    ) -> Result<()> {
274        self.post_topapi_unit(&["topapi", "v2", "department", "delete"], &request)
275            .await
276    }
277
278    /// Creates an approval process instance and returns its id.
279    pub async fn approval_create_process_instance(
280        &self,
281        request: ApprovalCreateProcessInstanceRequest,
282    ) -> Result<String> {
283        let access_token = self.get_access_token().await?;
284        let endpoint = self
285            .client
286            .webhook_endpoint(&["topapi", "processinstance", "create"])?;
287        parse_approval_create_response(
288            self.client
289                .webhook_http()
290                .post(endpoint.as_str())
291                .query_pair("access_token", access_token)
292                .json(&request)?
293                .send_response()
294                .await?,
295            self.client.body_snippet(),
296        )
297    }
298
299    /// Gets approval process instance details.
300    pub async fn approval_get_process_instance(
301        &self,
302        process_instance_id: &str,
303    ) -> Result<ApprovalProcessInstance> {
304        let access_token = self.get_access_token().await?;
305        let endpoint = self
306            .client
307            .webhook_endpoint(&["topapi", "processinstance", "get"])?;
308        let request = serde_json::json!({
309            "process_instance_id": process_instance_id
310        });
311        parse_approval_get_response(
312            self.client
313                .webhook_http()
314                .post(endpoint.as_str())
315                .query_pair("access_token", access_token)
316                .json(&request)?
317                .send_response()
318                .await?,
319            self.client.body_snippet(),
320        )
321    }
322
323    /// Lists approval process instance ids.
324    pub async fn approval_list_process_instance_ids(
325        &self,
326        request: ApprovalListProcessInstanceIdsRequest,
327    ) -> Result<ApprovalListProcessInstanceIdsResult> {
328        self.post_topapi_result(&["topapi", "processinstance", "listids"], &request)
329            .await
330    }
331
332    /// Terminates an approval process instance.
333    pub async fn approval_terminate_process_instance(
334        &self,
335        request: ApprovalTerminateProcessInstanceRequest,
336    ) -> Result<()> {
337        let body = serde_json::json!({ "request": request });
338        self.post_topapi_unit(&["topapi", "process", "instance", "terminate"], &body)
339            .await
340    }
341
342    /// Replies to an incoming callback message.
343    ///
344    /// For private chats, this sends OTO message to `senderStaffId`;
345    /// for group chats, it sends a group message to `conversationId`.
346    pub async fn reply_message(
347        &self,
348        data: &serde_json::Value,
349        title: &str,
350        text: &str,
351    ) -> Result<String> {
352        let msg_param = MsgParam {
353            title: title.to_string(),
354            text: text.to_string(),
355        };
356
357        if is_private_conversation(data) {
358            let sender_staff_id = data
359                .get("senderStaffId")
360                .and_then(|v| v.as_str())
361                .ok_or_else(|| Error::InvalidConfig {
362                    message: "Missing senderStaffId".to_string(),
363                    source: None,
364                })?;
365
366            let request = OtoMessageRequest {
367                msg_param,
368                msg_key: DEFAULT_MSG_KEY,
369                robot_code: &self.robot_code,
370                user_ids: vec![sender_staff_id],
371            };
372
373            self.send_enterprise_message(&["v1.0", "robot", "oToMessages", "batchSend"], &request)
374                .await
375        } else {
376            let conversation_id = data
377                .get("conversationId")
378                .and_then(|v| v.as_str())
379                .ok_or_else(|| Error::InvalidConfig {
380                    message: "Missing conversationId".to_string(),
381                    source: None,
382                })?;
383
384            let request = GroupMessageRequest {
385                msg_param,
386                msg_key: DEFAULT_MSG_KEY,
387                robot_code: &self.robot_code,
388                open_conversation_id: conversation_id,
389            };
390
391            self.send_enterprise_message(&["v1.0", "robot", "groupMessages", "send"], &request)
392                .await
393        }
394    }
395}