1use std::sync::Arc;
10
11use openlark_core::config::Config;
12#[cfg(any(feature = "im", feature = "contact"))]
13use openlark_core::error::business_error;
14#[cfg(feature = "im")]
15use openlark_core::validate_required;
16#[cfg(any(feature = "im", feature = "contact"))]
17use openlark_core::{SDKResult, error::validation_error};
18
19#[cfg(feature = "contact")]
20use crate::contact::contact::v3::user::{
21 create::UserResponse,
22 get::GetUserRequest,
23 models::{DepartmentIdType, UserIdType as ContactUserIdType},
24};
25#[cfg(feature = "contact")]
26use crate::contact::contact_search::old::default::v1::user::SearchUserRequest;
27#[cfg(feature = "im")]
28use crate::im::v1::message::{
29 create::{CreateMessageBody, CreateMessageRequest},
30 models::ReceiveIdType,
31 reply::{ReplyMessageBody, ReplyMessageRequest},
32};
33#[cfg(feature = "im")]
34use crate::im::v1::thread::forward::{ForwardThreadBody, ForwardThreadRequest};
35#[cfg(feature = "im")]
36use crate::im::v1::{
37 chat::{get::GetChatRequest, search::SearchChatsRequest},
38 message::models::UserIdType as ImUserIdType,
39};
40#[cfg(feature = "im")]
41use crate::im::v1::{
42 file::{
43 create::{CreateFileBody, CreateFileRequest},
44 models::CreateFileResponse,
45 },
46 image::{
47 create::CreateImageRequest,
48 models::{CreateImageResponse, ImageType},
49 },
50};
51
52#[cfg(feature = "im")]
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct MessageRecipient {
59 pub receive_id: String,
61 pub receive_id_type: ReceiveIdType,
63}
64
65#[cfg(feature = "im")]
66impl MessageRecipient {
67 pub fn new(receive_id: impl Into<String>, receive_id_type: ReceiveIdType) -> Self {
69 Self {
70 receive_id: receive_id.into(),
71 receive_id_type,
72 }
73 }
74
75 pub fn open_id(receive_id: impl Into<String>) -> Self {
77 Self::new(receive_id, ReceiveIdType::OpenId)
78 }
79
80 pub fn user_id(receive_id: impl Into<String>) -> Self {
82 Self::new(receive_id, ReceiveIdType::UserId)
83 }
84
85 pub fn email(receive_id: impl Into<String>) -> Self {
87 Self::new(receive_id, ReceiveIdType::Email)
88 }
89
90 pub fn chat_id(receive_id: impl Into<String>) -> Self {
92 Self::new(receive_id, ReceiveIdType::ChatId)
93 }
94}
95
96#[cfg(feature = "im")]
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct PostMessage {
102 pub locale: String,
104 pub title: String,
106 pub text: String,
108}
109
110#[cfg(feature = "im")]
111impl PostMessage {
112 pub fn zh_cn(title: impl Into<String>, text: impl Into<String>) -> Self {
114 Self {
115 locale: "zh_cn".to_string(),
116 title: title.into(),
117 text: text.into(),
118 }
119 }
120
121 fn into_content(self) -> SDKResult<String> {
122 let title = self.title.trim().to_string();
123 let text = self.text.trim().to_string();
124 if title.is_empty() {
125 return Err(validation_error("title", "title 不能为空"));
126 }
127 if text.is_empty() {
128 return Err(validation_error("text", "text 不能为空"));
129 }
130
131 Ok(serde_json::json!({
132 "post": {
133 self.locale: {
134 "title": title,
135 "content": [[{"tag": "text", "text": text}]]
136 }
137 }
138 })
139 .to_string())
140 }
141}
142
143#[cfg(feature = "im")]
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct ReplyTarget {
149 pub message_id: String,
151 pub reply_in_thread: bool,
153}
154
155#[cfg(feature = "im")]
156impl ReplyTarget {
157 pub fn direct(message_id: impl Into<String>) -> Self {
159 Self {
160 message_id: message_id.into(),
161 reply_in_thread: false,
162 }
163 }
164
165 pub fn in_thread(message_id: impl Into<String>) -> Self {
167 Self {
168 message_id: message_id.into(),
169 reply_in_thread: true,
170 }
171 }
172}
173
174#[cfg(feature = "im")]
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct MediaImageUpload {
181 pub image_type: ImageType,
183 pub file_name: Option<String>,
185 pub bytes: Vec<u8>,
187}
188
189#[cfg(feature = "im")]
190impl MediaImageUpload {
191 pub fn new(bytes: Vec<u8>) -> Self {
193 Self {
194 image_type: ImageType::Message,
195 file_name: None,
196 bytes,
197 }
198 }
199
200 pub fn avatar(mut self) -> Self {
202 self.image_type = ImageType::Avatar;
203 self
204 }
205
206 pub fn file_name(mut self, file_name: impl Into<String>) -> Self {
208 self.file_name = Some(file_name.into());
209 self
210 }
211}
212
213#[cfg(feature = "im")]
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct MediaFileUpload {
219 pub file_name: String,
221 pub file_type: String,
223 pub duration: Option<i32>,
225 pub bytes: Vec<u8>,
227}
228
229#[cfg(feature = "im")]
230impl MediaFileUpload {
231 pub fn new(file_name: impl Into<String>, bytes: Vec<u8>) -> Self {
233 let file_name = file_name.into();
234 let file_type = infer_file_type(&file_name);
235 Self {
236 file_name,
237 file_type,
238 duration: None,
239 bytes,
240 }
241 }
242
243 pub fn file_type(mut self, file_type: impl Into<String>) -> Self {
245 self.file_type = file_type.into();
246 self
247 }
248
249 pub fn duration(mut self, duration: i32) -> Self {
251 self.duration = Some(duration);
252 self
253 }
254}
255
256#[cfg(feature = "contact")]
260#[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq)]
261pub struct UserLookupItem {
262 pub name: String,
264 pub open_id: String,
266 #[serde(default)]
268 pub user_id: Option<String>,
269 #[serde(default)]
271 pub department_ids: Vec<String>,
272}
273
274#[cfg(feature = "im")]
278#[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq)]
279pub struct ChatLookupItem {
280 pub chat_id: String,
282 pub name: String,
284 #[serde(default)]
286 pub description: Option<String>,
287 #[serde(default)]
289 pub owner_id: Option<String>,
290 #[serde(default)]
292 pub owner_id_type: Option<String>,
293 #[serde(default)]
295 pub external: bool,
296 #[serde(default)]
298 pub tenant_key: Option<String>,
299 #[serde(default)]
301 pub chat_status: Option<String>,
302}
303
304#[cfg(feature = "contact")]
305#[derive(Debug, Clone, serde::Deserialize)]
306struct UserLookupResponse {
307 #[serde(default)]
308 has_more: bool,
309 #[serde(default)]
310 page_token: Option<String>,
311 #[serde(default)]
312 users: Vec<UserLookupItem>,
313}
314
315#[cfg(feature = "im")]
316#[derive(Debug, Clone, serde::Deserialize)]
317struct ChatLookupResponse {
318 #[serde(default)]
319 has_more: bool,
320 #[serde(default)]
321 page_token: Option<String>,
322 #[serde(default)]
323 items: Vec<ChatLookupItem>,
324}
325
326#[derive(Debug, Clone)]
328pub struct CommunicationClient {
329 config: Arc<Config>,
330 #[cfg(feature = "aily")]
331 pub aily: AilyClient,
333
334 #[cfg(feature = "im")]
335 pub im: ImClient,
337
338 #[cfg(feature = "contact")]
339 pub contact: ContactClient,
341
342 #[cfg(feature = "moments")]
343 pub moments: MomentsClient,
345}
346
347#[cfg(feature = "aily")]
348#[derive(Debug, Clone)]
350pub struct AilyClient {
351 config: Arc<Config>,
352}
353
354#[cfg(feature = "aily")]
355impl AilyClient {
356 fn new(config: Arc<Config>) -> Self {
357 Self { config }
358 }
359
360 pub fn config(&self) -> &Config {
362 &self.config
363 }
364
365 pub fn aily_session(&self) -> AilySessionResource {
367 AilySessionResource::new(self.config.clone())
368 }
369
370 pub fn app(&self) -> AppResource {
372 AppResource::new(self.config.clone())
373 }
374
375 pub fn agent(&self) -> AgentResource {
377 AgentResource::new(self.config.clone())
378 }
379
380 pub fn tenant(&self) -> TenantResource {
382 TenantResource::new(self.config.clone())
383 }
384}
385
386#[cfg(feature = "aily")]
388#[derive(Debug, Clone)]
389pub struct AilySessionResource {
390 config: Arc<Config>,
391}
392
393#[cfg(feature = "aily")]
394impl AilySessionResource {
395 fn new(config: Arc<Config>) -> Self {
396 Self { config }
397 }
398
399 pub fn create(&self) -> crate::aily::aily::v1::aily_session::create::CreateSessionRequest {
401 crate::aily::aily::v1::aily_session::create::CreateSessionRequest::new(
402 (*self.config).clone(),
403 )
404 }
405
406 pub fn delete(
408 &self,
409 session_id: impl Into<String>,
410 ) -> crate::aily::aily::v1::aily_session::delete::DeleteSessionRequest {
411 crate::aily::aily::v1::aily_session::delete::DeleteSessionRequest::new(
412 (*self.config).clone(),
413 )
414 .aily_session_id(session_id)
415 }
416
417 pub fn get(
419 &self,
420 session_id: impl Into<String>,
421 ) -> crate::aily::aily::v1::aily_session::get::GetSessionRequest {
422 crate::aily::aily::v1::aily_session::get::GetSessionRequest::new((*self.config).clone())
423 .aily_session_id(session_id)
424 }
425
426 pub fn update(
428 &self,
429 session_id: impl Into<String>,
430 ) -> crate::aily::aily::v1::aily_session::update::UpdateSessionRequest {
431 crate::aily::aily::v1::aily_session::update::UpdateSessionRequest::new(
432 (*self.config).clone(),
433 )
434 .aily_session_id(session_id)
435 }
436
437 pub fn aily_message(&self) -> AilyMessageResource {
439 AilyMessageResource::new(self.config.clone())
440 }
441
442 pub fn run(&self) -> RunResource {
444 RunResource::new(self.config.clone())
445 }
446}
447
448#[cfg(feature = "aily")]
450#[derive(Debug, Clone)]
451pub struct AilyMessageResource {
452 config: Arc<Config>,
453}
454
455#[cfg(feature = "aily")]
456impl AilyMessageResource {
457 fn new(config: Arc<Config>) -> Self {
458 Self { config }
459 }
460
461 pub fn create(
463 &self,
464 session_id: impl Into<String>,
465 ) -> crate::aily::aily::v1::aily_session::aily_message::create::CreateAilyMessageRequest {
466 crate::aily::aily::v1::aily_session::aily_message::create::CreateAilyMessageRequest::new(
467 (*self.config).clone(),
468 )
469 .aily_session_id(session_id)
470 }
471
472 pub fn get(
474 &self,
475 session_id: impl Into<String>,
476 message_id: impl Into<String>,
477 ) -> crate::aily::aily::v1::aily_session::aily_message::get::GetMessageRequest {
478 crate::aily::aily::v1::aily_session::aily_message::get::GetMessageRequest::new(
479 (*self.config).clone(),
480 )
481 .aily_session_id(session_id)
482 .aily_message_id(message_id)
483 }
484
485 pub fn list(
487 &self,
488 session_id: impl Into<String>,
489 ) -> crate::aily::aily::v1::aily_session::aily_message::list::ListAilyMessagesRequest {
490 crate::aily::aily::v1::aily_session::aily_message::list::ListAilyMessagesRequest::new(
491 (*self.config).clone(),
492 )
493 .aily_session_id(session_id)
494 }
495}
496
497#[cfg(feature = "aily")]
499#[derive(Debug, Clone)]
500pub struct RunResource {
501 config: Arc<Config>,
502}
503
504#[cfg(feature = "aily")]
505impl RunResource {
506 fn new(config: Arc<Config>) -> Self {
507 Self { config }
508 }
509
510 pub fn cancel(
512 &self,
513 session_id: impl Into<String>,
514 run_id: impl Into<String>,
515 ) -> crate::aily::aily::v1::aily_session::run::cancel::CancelRunRequest {
516 crate::aily::aily::v1::aily_session::run::cancel::CancelRunRequest::new(
517 (*self.config).clone(),
518 )
519 .aily_session_id(session_id)
520 .run_id(run_id)
521 }
522
523 pub fn create(
525 &self,
526 session_id: impl Into<String>,
527 ) -> crate::aily::aily::v1::aily_session::run::create::CreateRunRequest {
528 crate::aily::aily::v1::aily_session::run::create::CreateRunRequest::new(
529 (*self.config).clone(),
530 )
531 .aily_session_id(session_id)
532 }
533
534 pub fn get(
536 &self,
537 session_id: impl Into<String>,
538 run_id: impl Into<String>,
539 ) -> crate::aily::aily::v1::aily_session::run::get::GetRunRequest {
540 crate::aily::aily::v1::aily_session::run::get::GetRunRequest::new((*self.config).clone())
541 .aily_session_id(session_id)
542 .run_id(run_id)
543 }
544
545 pub fn list(
547 &self,
548 session_id: impl Into<String>,
549 ) -> crate::aily::aily::v1::aily_session::run::list::ListRunsRequest {
550 crate::aily::aily::v1::aily_session::run::list::ListRunsRequest::new((*self.config).clone())
551 .aily_session_id(session_id)
552 }
553}
554
555#[cfg(feature = "aily")]
557#[derive(Debug, Clone)]
558pub struct AppResource {
559 config: Arc<Config>,
560}
561
562#[cfg(feature = "aily")]
563impl AppResource {
564 fn new(config: Arc<Config>) -> Self {
565 Self { config }
566 }
567
568 pub fn data_asset(&self) -> DataAssetResource {
570 DataAssetResource::new(self.config.clone())
571 }
572
573 pub fn data_asset_tag(&self) -> DataAssetTagResource {
575 DataAssetTagResource::new(self.config.clone())
576 }
577
578 pub fn knowledge(&self) -> KnowledgeResource {
580 KnowledgeResource::new(self.config.clone())
581 }
582
583 pub fn skill(&self) -> SkillResource {
585 SkillResource::new(self.config.clone())
586 }
587}
588
589#[cfg(feature = "aily")]
591#[derive(Debug, Clone)]
592pub struct DataAssetResource {
593 config: Arc<Config>,
594}
595
596#[cfg(feature = "aily")]
597impl DataAssetResource {
598 fn new(config: Arc<Config>) -> Self {
599 Self { config }
600 }
601
602 pub fn create(
604 &self,
605 app_id: impl Into<String>,
606 ) -> crate::aily::aily::v1::app::data_asset::create::CreateDataAssetRequest {
607 crate::aily::aily::v1::app::data_asset::create::CreateDataAssetRequest::new(
608 (*self.config).clone(),
609 )
610 .app_id(app_id)
611 }
612
613 pub fn delete(
615 &self,
616 app_id: impl Into<String>,
617 data_asset_id: impl Into<String>,
618 ) -> crate::aily::aily::v1::app::data_asset::delete::DeleteDataAssetRequest {
619 crate::aily::aily::v1::app::data_asset::delete::DeleteDataAssetRequest::new(
620 (*self.config).clone(),
621 )
622 .app_id(app_id)
623 .data_asset_id(data_asset_id)
624 }
625
626 pub fn get(
628 &self,
629 app_id: impl Into<String>,
630 data_asset_id: impl Into<String>,
631 ) -> crate::aily::aily::v1::app::data_asset::get::GetDataAssetRequest {
632 crate::aily::aily::v1::app::data_asset::get::GetDataAssetRequest::new(
633 (*self.config).clone(),
634 )
635 .app_id(app_id)
636 .data_asset_id(data_asset_id)
637 }
638
639 pub fn list(
641 &self,
642 app_id: impl Into<String>,
643 ) -> crate::aily::aily::v1::app::data_asset::list::ListDataAssetsRequest {
644 crate::aily::aily::v1::app::data_asset::list::ListDataAssetsRequest::new(
645 (*self.config).clone(),
646 )
647 .app_id(app_id)
648 }
649
650 pub fn upload_file(
652 &self,
653 app_id: impl Into<String>,
654 ) -> crate::aily::aily::v1::app::data_asset::upload_file::UploadFileRequest {
655 crate::aily::aily::v1::app::data_asset::upload_file::UploadFileRequest::new(
656 (*self.config).clone(),
657 )
658 .app_id(app_id)
659 }
660}
661
662#[cfg(feature = "aily")]
664#[derive(Debug, Clone)]
665pub struct DataAssetTagResource {
666 config: Arc<Config>,
667}
668
669#[cfg(feature = "aily")]
670impl DataAssetTagResource {
671 fn new(config: Arc<Config>) -> Self {
672 Self { config }
673 }
674
675 pub fn list(
677 &self,
678 app_id: impl Into<String>,
679 ) -> crate::aily::aily::v1::app::data_asset_tag::list::ListDataAssetTagsRequest {
680 crate::aily::aily::v1::app::data_asset_tag::list::ListDataAssetTagsRequest::new(
681 (*self.config).clone(),
682 )
683 .app_id(app_id)
684 }
685}
686
687#[cfg(feature = "aily")]
689#[derive(Debug, Clone)]
690pub struct KnowledgeResource {
691 config: Arc<Config>,
692}
693
694#[cfg(feature = "aily")]
695impl KnowledgeResource {
696 fn new(config: Arc<Config>) -> Self {
697 Self { config }
698 }
699
700 pub fn ask(
702 &self,
703 app_id: impl Into<String>,
704 ) -> crate::aily::aily::v1::app::knowledge::ask::AskKnowledgeRequest {
705 crate::aily::aily::v1::app::knowledge::ask::AskKnowledgeRequest::new((*self.config).clone())
706 .app_id(app_id)
707 }
708}
709
710#[cfg(feature = "aily")]
712#[derive(Debug, Clone)]
713pub struct SkillResource {
714 config: Arc<Config>,
715}
716
717#[cfg(feature = "aily")]
718impl SkillResource {
719 fn new(config: Arc<Config>) -> Self {
720 Self { config }
721 }
722
723 pub fn get(
725 &self,
726 app_id: impl Into<String>,
727 skill_id: impl Into<String>,
728 ) -> crate::aily::aily::v1::app::skill::get::GetSkillRequest {
729 crate::aily::aily::v1::app::skill::get::GetSkillRequest::new((*self.config).clone())
730 .app_id(app_id)
731 .skill_id(skill_id)
732 }
733
734 pub fn list(
736 &self,
737 app_id: impl Into<String>,
738 ) -> crate::aily::aily::v1::app::skill::list::ListSkillsRequest {
739 crate::aily::aily::v1::app::skill::list::ListSkillsRequest::new((*self.config).clone())
740 .app_id(app_id)
741 }
742
743 pub fn start(
745 &self,
746 app_id: impl Into<String>,
747 skill_id: impl Into<String>,
748 ) -> crate::aily::aily::v1::app::skill::start::StartSkillRequest {
749 crate::aily::aily::v1::app::skill::start::StartSkillRequest::new((*self.config).clone())
750 .app_id(app_id)
751 .skill_id(skill_id)
752 }
753}
754
755#[cfg(feature = "aily")]
757#[derive(Debug, Clone)]
758pub struct AgentResource {
759 config: Arc<Config>,
760}
761
762#[cfg(feature = "aily")]
763impl AgentResource {
764 fn new(config: Arc<Config>) -> Self {
765 Self { config }
766 }
767
768 pub fn agent_artifact(&self) -> AgentArtifactResource {
770 AgentArtifactResource::new(self.config.clone())
771 }
772
773 pub fn agent_attachment(&self) -> AgentAttachmentResource {
775 AgentAttachmentResource::new(self.config.clone())
776 }
777
778 pub fn agent_chat(&self) -> AgentChatResource {
780 AgentChatResource::new(self.config.clone())
781 }
782
783 pub fn agent_chat_session(&self) -> AgentChatSessionResource {
785 AgentChatSessionResource::new(self.config.clone())
786 }
787
788 pub fn agent_visibility(&self) -> AgentVisibilityResource {
790 AgentVisibilityResource::new(self.config.clone())
791 }
792}
793
794#[cfg(feature = "aily")]
796#[derive(Debug, Clone)]
797pub struct AgentArtifactResource {
798 config: Arc<Config>,
799}
800
801#[cfg(feature = "aily")]
802impl AgentArtifactResource {
803 fn new(config: Arc<Config>) -> Self {
804 Self { config }
805 }
806
807 pub fn get(
809 &self,
810 agent_id: impl Into<String>,
811 agent_artifact_id: impl Into<String>,
812 ) -> crate::aily::aily::v1::agent::agent_artifact::get::GetAgentArtifactRequest {
813 crate::aily::aily::v1::agent::agent_artifact::get::GetAgentArtifactRequest::new(
814 self.config.clone(),
815 )
816 .agent_id(agent_id)
817 .agent_artifact_id(agent_artifact_id)
818 }
819}
820
821#[cfg(feature = "aily")]
823#[derive(Debug, Clone)]
824pub struct AgentAttachmentResource {
825 config: Arc<Config>,
826}
827
828#[cfg(feature = "aily")]
829impl AgentAttachmentResource {
830 fn new(config: Arc<Config>) -> Self {
831 Self { config }
832 }
833
834 pub fn create(
836 &self,
837 agent_id: impl Into<String>,
838 ) -> crate::aily::aily::v1::agent::agent_attachment::create::CreateAgentAttachmentRequest {
839 crate::aily::aily::v1::agent::agent_attachment::create::CreateAgentAttachmentRequest::new(
840 self.config.clone(),
841 )
842 .agent_id(agent_id)
843 }
844}
845
846#[cfg(feature = "aily")]
848#[derive(Debug, Clone)]
849pub struct AgentChatResource {
850 config: Arc<Config>,
851}
852
853#[cfg(feature = "aily")]
854impl AgentChatResource {
855 fn new(config: Arc<Config>) -> Self {
856 Self { config }
857 }
858
859 pub fn create(
861 &self,
862 agent_id: impl Into<String>,
863 ) -> crate::aily::aily::v1::agent::agent_chat::create::CreateAgentChatRequest {
864 crate::aily::aily::v1::agent::agent_chat::create::CreateAgentChatRequest::new(
865 self.config.clone(),
866 )
867 .agent_id(agent_id)
868 }
869
870 pub fn get(
872 &self,
873 agent_id: impl Into<String>,
874 agent_chat_id: impl Into<String>,
875 ) -> crate::aily::aily::v1::agent::agent_chat::get::GetAgentChatRequest {
876 crate::aily::aily::v1::agent::agent_chat::get::GetAgentChatRequest::new(self.config.clone())
877 .agent_id(agent_id)
878 .agent_chat_id(agent_chat_id)
879 }
880}
881
882#[cfg(feature = "aily")]
884#[derive(Debug, Clone)]
885pub struct AgentChatSessionResource {
886 config: Arc<Config>,
887}
888
889#[cfg(feature = "aily")]
890impl AgentChatSessionResource {
891 fn new(config: Arc<Config>) -> Self {
892 Self { config }
893 }
894
895 pub fn create(
897 &self,
898 agent_id: impl Into<String>,
899 ) -> crate::aily::aily::v1::agent::agent_chat_session::create::CreateAgentChatSessionRequest
900 {
901 crate::aily::aily::v1::agent::agent_chat_session::create::CreateAgentChatSessionRequest::new(
902 self.config.clone(),
903 )
904 .agent_id(agent_id)
905 }
906
907 pub fn delete(
909 &self,
910 agent_id: impl Into<String>,
911 agent_chat_session_id: impl Into<String>,
912 ) -> crate::aily::aily::v1::agent::agent_chat_session::delete::DeleteAgentChatSessionRequest
913 {
914 crate::aily::aily::v1::agent::agent_chat_session::delete::DeleteAgentChatSessionRequest::new(
915 self.config.clone(),
916 )
917 .agent_id(agent_id)
918 .agent_chat_session_id(agent_chat_session_id)
919 }
920
921 pub fn get(
923 &self,
924 agent_id: impl Into<String>,
925 agent_chat_session_id: impl Into<String>,
926 ) -> crate::aily::aily::v1::agent::agent_chat_session::get::GetAgentChatSessionRequest {
927 crate::aily::aily::v1::agent::agent_chat_session::get::GetAgentChatSessionRequest::new(
928 self.config.clone(),
929 )
930 .agent_id(agent_id)
931 .agent_chat_session_id(agent_chat_session_id)
932 }
933
934 pub fn list(
936 &self,
937 agent_id: impl Into<String>,
938 ) -> crate::aily::aily::v1::agent::agent_chat_session::list::ListAgentChatSessionRequest {
939 crate::aily::aily::v1::agent::agent_chat_session::list::ListAgentChatSessionRequest::new(
940 self.config.clone(),
941 )
942 .agent_id(agent_id)
943 }
944}
945
946#[cfg(feature = "aily")]
948#[derive(Debug, Clone)]
949pub struct AgentVisibilityResource {
950 config: Arc<Config>,
951}
952
953#[cfg(feature = "aily")]
954impl AgentVisibilityResource {
955 fn new(config: Arc<Config>) -> Self {
956 Self { config }
957 }
958
959 pub fn check(
961 &self,
962 agent_id: impl Into<String>,
963 ) -> crate::aily::aily::v1::agent::agent_visibility::check::CheckAgentVisibilityRequest {
964 crate::aily::aily::v1::agent::agent_visibility::check::CheckAgentVisibilityRequest::new(
965 self.config.clone(),
966 )
967 .agent_id(agent_id)
968 }
969}
970
971#[cfg(feature = "aily")]
973#[derive(Debug, Clone)]
974pub struct TenantResource {
975 config: Arc<Config>,
976}
977
978#[cfg(feature = "aily")]
979impl TenantResource {
980 fn new(config: Arc<Config>) -> Self {
981 Self { config }
982 }
983
984 pub fn app_stat(&self) -> AppStatResource {
986 AppStatResource::new(self.config.clone())
987 }
988}
989
990#[cfg(feature = "aily")]
992#[derive(Debug, Clone)]
993pub struct AppStatResource {
994 config: Arc<Config>,
995}
996
997#[cfg(feature = "aily")]
998impl AppStatResource {
999 fn new(config: Arc<Config>) -> Self {
1000 Self { config }
1001 }
1002
1003 pub fn list(&self) -> crate::aily::aily::v1::tenant::app_stat::list::ListAppStatsRequest {
1005 crate::aily::aily::v1::tenant::app_stat::list::ListAppStatsRequest::new(
1006 (*self.config).clone(),
1007 )
1008 }
1009}
1010
1011impl CommunicationClient {
1012 pub fn new(config: Config) -> Self {
1014 let config = Arc::new(config);
1015 Self {
1016 config: config.clone(),
1017 #[cfg(feature = "aily")]
1018 aily: AilyClient::new(config.clone()),
1019 #[cfg(feature = "im")]
1020 im: ImClient::new(config.clone()),
1021 #[cfg(feature = "contact")]
1022 contact: ContactClient::new(config.clone()),
1023 #[cfg(feature = "moments")]
1024 moments: MomentsClient::new(config),
1025 }
1026 }
1027
1028 pub fn config(&self) -> &Config {
1030 &self.config
1031 }
1032}
1033
1034#[cfg(feature = "im")]
1035#[derive(Debug, Clone)]
1037pub struct ImClient {
1038 config: Arc<Config>,
1039}
1040
1041#[cfg(feature = "im")]
1042impl ImClient {
1043 fn new(config: Arc<Config>) -> Self {
1044 Self { config }
1045 }
1046
1047 pub fn config(&self) -> &Config {
1049 &self.config
1050 }
1051
1052 pub async fn send_text(
1054 &self,
1055 recipient: MessageRecipient,
1056 text: impl Into<String>,
1057 ) -> SDKResult<serde_json::Value> {
1058 let body = Self::build_text_body(recipient, text.into())?;
1059 Self::create_message_request(self.config.clone(), body.receive_id_type())
1060 .execute(body.into())
1061 .await
1062 }
1063
1064 pub async fn send_post(
1066 &self,
1067 recipient: MessageRecipient,
1068 post: PostMessage,
1069 ) -> SDKResult<serde_json::Value> {
1070 let body = Self::build_post_body(recipient, post)?;
1071 Self::create_message_request(self.config.clone(), body.receive_id_type())
1072 .execute(body.into())
1073 .await
1074 }
1075
1076 pub async fn reply_text(
1078 &self,
1079 target: ReplyTarget,
1080 text: impl Into<String>,
1081 ) -> SDKResult<serde_json::Value> {
1082 let body = Self::build_reply_text_body(target, text.into())?;
1083 Self::create_reply_request(self.config.clone(), body.message_id())
1084 .execute(body.into())
1085 .await
1086 }
1087
1088 pub async fn reply_post(
1090 &self,
1091 target: ReplyTarget,
1092 post: PostMessage,
1093 ) -> SDKResult<serde_json::Value> {
1094 let body = Self::build_reply_post_body(target, post)?;
1095 Self::create_reply_request(self.config.clone(), body.message_id())
1096 .execute(body.into())
1097 .await
1098 }
1099
1100 pub async fn forward_thread(
1102 &self,
1103 thread_id: impl Into<String>,
1104 recipient: MessageRecipient,
1105 ) -> SDKResult<serde_json::Value> {
1106 let request = ForwardThreadRequest::new(self.config.as_ref().clone())
1107 .thread_id(thread_id)
1108 .receive_id_type(recipient.receive_id_type);
1109 request
1110 .execute(ForwardThreadBody::new(recipient.receive_id))
1111 .await
1112 }
1113
1114 pub async fn upload_image(&self, upload: MediaImageUpload) -> SDKResult<CreateImageResponse> {
1116 if upload.bytes.is_empty() {
1117 return Err(validation_error("image", "image 不能为空"));
1118 }
1119 let mut request =
1120 CreateImageRequest::new(self.config.as_ref().clone()).image_type(upload.image_type);
1121 if let Some(file_name) = upload.file_name {
1122 request = request.file_name(file_name);
1123 }
1124 request.execute(upload.bytes).await
1125 }
1126
1127 pub async fn upload_file(&self, upload: MediaFileUpload) -> SDKResult<CreateFileResponse> {
1129 let mut body = CreateFileBody::new(upload.file_type, upload.file_name);
1130 if let Some(duration) = upload.duration {
1131 body = body.duration(duration);
1132 }
1133 CreateFileRequest::new(self.config.as_ref().clone())
1134 .execute(body, upload.bytes)
1135 .await
1136 }
1137
1138 pub async fn send_image(
1140 &self,
1141 recipient: MessageRecipient,
1142 image_key: impl Into<String>,
1143 ) -> SDKResult<serde_json::Value> {
1144 let image_key = image_key.into();
1145 if image_key.trim().is_empty() {
1146 return Err(validation_error("image_key", "image_key 不能为空"));
1147 }
1148 let body = Self::build_media_body(
1149 recipient,
1150 "image",
1151 serde_json::json!({ "image_key": image_key }).to_string(),
1152 )?;
1153 Self::create_message_request(self.config.clone(), body.receive_id_type())
1154 .execute(body.into())
1155 .await
1156 }
1157
1158 pub async fn send_file(
1160 &self,
1161 recipient: MessageRecipient,
1162 file_key: impl Into<String>,
1163 ) -> SDKResult<serde_json::Value> {
1164 let file_key = file_key.into();
1165 if file_key.trim().is_empty() {
1166 return Err(validation_error("file_key", "file_key 不能为空"));
1167 }
1168 let body = Self::build_media_body(
1169 recipient,
1170 "file",
1171 serde_json::json!({ "file_key": file_key }).to_string(),
1172 )?;
1173 Self::create_message_request(self.config.clone(), body.receive_id_type())
1174 .execute(body.into())
1175 .await
1176 }
1177
1178 pub async fn search_chats_all(&self, query: impl AsRef<str>) -> SDKResult<Vec<ChatLookupItem>> {
1180 let query = query.as_ref().trim().to_string();
1181 if query.is_empty() {
1182 return Err(validation_error("query", "query 不能为空"));
1183 }
1184
1185 let mut items = Vec::new();
1186 let mut page_token: Option<String> = None;
1187
1188 loop {
1189 let mut request = SearchChatsRequest::new(self.config.as_ref().clone())
1190 .query(query.clone())
1191 .user_id_type(ImUserIdType::OpenId)
1192 .page_size(100);
1193 if let Some(token) = &page_token {
1194 request = request.page_token(token.clone());
1195 }
1196
1197 let response: ChatLookupResponse = serde_json::from_value(request.execute().await?)
1198 .map_err(|e| validation_error("chat_lookup_response", e.to_string().as_str()))?;
1199 items.extend(response.items);
1200
1201 if !response.has_more {
1202 break;
1203 }
1204 page_token = response.page_token;
1205 }
1206
1207 Ok(items)
1208 }
1209
1210 pub async fn find_chat_by_name(&self, name: &str) -> SDKResult<ChatLookupItem> {
1212 let items = self.search_chats_all(name).await?;
1213 find_unique_chat_by_name(&items, name)
1214 }
1215
1216 pub async fn get_chat_info(&self, chat_id: impl Into<String>) -> SDKResult<serde_json::Value> {
1218 GetChatRequest::new(self.config.as_ref().clone())
1219 .chat_id(chat_id)
1220 .user_id_type(ImUserIdType::OpenId)
1221 .execute()
1222 .await
1223 }
1224
1225 fn create_message_request(
1226 config: Arc<Config>,
1227 receive_id_type: ReceiveIdType,
1228 ) -> CreateMessageRequest {
1229 CreateMessageRequest::new(config.as_ref().clone()).receive_id_type(receive_id_type)
1230 }
1231
1232 fn create_reply_request(config: Arc<Config>, message_id: String) -> ReplyMessageRequest {
1233 ReplyMessageRequest::new(config.as_ref().clone()).message_id(message_id)
1234 }
1235
1236 fn build_text_body(recipient: MessageRecipient, text: String) -> SDKResult<HelperMessageBody> {
1237 let text = text.trim().to_string();
1238 if text.is_empty() {
1239 return Err(validation_error("text", "text 不能为空"));
1240 }
1241
1242 Ok(HelperMessageBody::new(
1243 recipient,
1244 "text",
1245 serde_json::json!({ "text": text }).to_string(),
1246 ))
1247 }
1248
1249 fn build_post_body(
1250 recipient: MessageRecipient,
1251 post: PostMessage,
1252 ) -> SDKResult<HelperMessageBody> {
1253 Ok(HelperMessageBody::new(
1254 recipient,
1255 "post",
1256 post.into_content()?,
1257 ))
1258 }
1259
1260 fn build_media_body(
1261 recipient: MessageRecipient,
1262 msg_type: &str,
1263 content: String,
1264 ) -> SDKResult<HelperMessageBody> {
1265 validate_required!(content, "content 不能为空");
1266 Ok(HelperMessageBody::new(recipient, msg_type, content))
1267 }
1268
1269 fn build_reply_text_body(target: ReplyTarget, text: String) -> SDKResult<HelperReplyBody> {
1270 let text = text.trim().to_string();
1271 if text.is_empty() {
1272 return Err(validation_error("text", "text 不能为空"));
1273 }
1274
1275 Ok(HelperReplyBody::new(
1276 target,
1277 "text",
1278 serde_json::json!({ "text": text }).to_string(),
1279 ))
1280 }
1281
1282 fn build_reply_post_body(target: ReplyTarget, post: PostMessage) -> SDKResult<HelperReplyBody> {
1283 Ok(HelperReplyBody::new(target, "post", post.into_content()?))
1284 }
1285}
1286
1287#[cfg(feature = "im")]
1288#[derive(Debug, Clone)]
1289struct HelperMessageBody {
1290 body: CreateMessageBody,
1291 receive_id_type: ReceiveIdType,
1292}
1293
1294#[cfg(feature = "im")]
1295impl HelperMessageBody {
1296 fn new(recipient: MessageRecipient, msg_type: &str, content: String) -> Self {
1297 Self {
1298 receive_id_type: recipient.receive_id_type,
1299 body: CreateMessageBody {
1300 receive_id: recipient.receive_id,
1301 msg_type: msg_type.to_string(),
1302 content,
1303 uuid: None,
1304 },
1305 }
1306 }
1307
1308 fn receive_id_type(&self) -> ReceiveIdType {
1309 self.receive_id_type
1310 }
1311}
1312
1313#[cfg(feature = "im")]
1314impl From<HelperMessageBody> for CreateMessageBody {
1315 fn from(value: HelperMessageBody) -> Self {
1316 value.body
1317 }
1318}
1319
1320#[cfg(feature = "im")]
1321#[derive(Debug, Clone)]
1322struct HelperReplyBody {
1323 body: ReplyMessageBody,
1324 message_id: String,
1325}
1326
1327#[cfg(feature = "im")]
1328impl HelperReplyBody {
1329 fn new(target: ReplyTarget, msg_type: &str, content: String) -> Self {
1330 Self {
1331 message_id: target.message_id,
1332 body: ReplyMessageBody {
1333 content,
1334 msg_type: msg_type.to_string(),
1335 reply_in_thread: Some(target.reply_in_thread),
1336 uuid: None,
1337 },
1338 }
1339 }
1340
1341 fn message_id(&self) -> String {
1342 self.message_id.clone()
1343 }
1344}
1345
1346#[cfg(feature = "im")]
1347impl From<HelperReplyBody> for ReplyMessageBody {
1348 fn from(value: HelperReplyBody) -> Self {
1349 value.body
1350 }
1351}
1352
1353#[cfg(feature = "im")]
1354fn infer_file_type(file_name: &str) -> String {
1355 std::path::Path::new(file_name)
1356 .extension()
1357 .and_then(|ext| ext.to_str())
1358 .map(|ext| ext.to_ascii_lowercase())
1359 .filter(|ext| !ext.is_empty())
1360 .unwrap_or_else(|| "stream".to_string())
1361}
1362
1363#[cfg(feature = "contact")]
1364fn find_unique_user_by_name(users: &[UserLookupItem], name: &str) -> SDKResult<UserLookupItem> {
1365 let name = name.trim();
1366 if name.is_empty() {
1367 return Err(validation_error("name", "name 不能为空"));
1368 }
1369
1370 let mut matches = users.iter().filter(|user| user.name == name).cloned();
1371
1372 let first = matches
1373 .next()
1374 .ok_or_else(|| business_error(format!("未找到用户: {name}")))?;
1375 if matches.next().is_some() {
1376 return Err(business_error(format!(
1377 "找到多个同名用户,请缩小范围: {name}"
1378 )));
1379 }
1380 Ok(first)
1381}
1382
1383#[cfg(feature = "im")]
1384fn find_unique_chat_by_name(chats: &[ChatLookupItem], name: &str) -> SDKResult<ChatLookupItem> {
1385 let name = name.trim();
1386 if name.is_empty() {
1387 return Err(validation_error("name", "name 不能为空"));
1388 }
1389
1390 let mut matches = chats.iter().filter(|chat| chat.name == name).cloned();
1391
1392 let first = matches
1393 .next()
1394 .ok_or_else(|| business_error(format!("未找到群聊: {name}")))?;
1395 if matches.next().is_some() {
1396 return Err(business_error(format!(
1397 "找到多个同名群聊,请缩小范围: {name}"
1398 )));
1399 }
1400 Ok(first)
1401}
1402
1403#[cfg(feature = "contact")]
1404#[derive(Debug, Clone)]
1406pub struct ContactClient {
1407 config: Arc<Config>,
1408}
1409
1410#[cfg(feature = "contact")]
1411impl ContactClient {
1412 fn new(config: Arc<Config>) -> Self {
1413 Self { config }
1414 }
1415
1416 pub fn config(&self) -> &Config {
1418 &self.config
1419 }
1420
1421 pub async fn search_users_all(&self, query: impl AsRef<str>) -> SDKResult<Vec<UserLookupItem>> {
1423 let query = query.as_ref().trim().to_string();
1424 if query.is_empty() {
1425 return Err(validation_error("query", "query 不能为空"));
1426 }
1427
1428 let mut users = Vec::new();
1429 let mut page_token: Option<String> = None;
1430
1431 loop {
1432 let mut request = SearchUserRequest::new(self.config.as_ref().clone())
1433 .query(query.clone())
1434 .page_size(100);
1435 if let Some(token) = &page_token {
1436 request = request.page_token(token.clone());
1437 }
1438
1439 let response: UserLookupResponse = serde_json::from_value(request.execute().await?)
1440 .map_err(|e| validation_error("user_lookup_response", e.to_string().as_str()))?;
1441 users.extend(response.users);
1442
1443 if !response.has_more {
1444 break;
1445 }
1446 page_token = response.page_token;
1447 }
1448
1449 Ok(users)
1450 }
1451
1452 pub async fn find_user_by_name(&self, name: &str) -> SDKResult<UserLookupItem> {
1454 let users = self.search_users_all(name).await?;
1455 find_unique_user_by_name(&users, name)
1456 }
1457
1458 pub async fn get_user_by_open_id(&self, open_id: impl Into<String>) -> SDKResult<UserResponse> {
1460 GetUserRequest::new(self.config.as_ref().clone())
1461 .user_id(open_id)
1462 .user_id_type(ContactUserIdType::OpenId)
1463 .department_id_type(DepartmentIdType::OpenDepartmentId)
1464 .execute()
1465 .await
1466 }
1467}
1468
1469#[cfg(feature = "moments")]
1470#[derive(Debug, Clone)]
1472pub struct MomentsClient {
1473 config: Arc<Config>,
1474}
1475
1476#[cfg(feature = "moments")]
1477impl MomentsClient {
1478 fn new(config: Arc<Config>) -> Self {
1479 Self { config }
1480 }
1481
1482 pub fn config(&self) -> &Config {
1484 &self.config
1485 }
1486}
1487
1488#[cfg(test)]
1489#[allow(unused_imports)]
1490mod tests {
1491 use super::*;
1492
1493 fn create_test_config() -> Config {
1494 Config::builder()
1495 .app_id("test_app")
1496 .app_secret("test_secret")
1497 .build()
1498 }
1499
1500 #[test]
1501 fn test_communication_client_creation() {
1502 let config = create_test_config();
1503 let client = CommunicationClient::new(config);
1504 assert_eq!(client.config().app_id(), "test_app");
1505 }
1506
1507 #[test]
1508 fn test_communication_client_debug() {
1509 let config = create_test_config();
1510 let client = CommunicationClient::new(config);
1511 let debug_str = format!("{client:?}");
1512 assert!(debug_str.contains("CommunicationClient"));
1513 }
1514
1515 #[test]
1516 fn test_communication_client_clone() {
1517 let config = create_test_config();
1518 let client = CommunicationClient::new(config);
1519 let cloned = client.clone();
1520 assert_eq!(cloned.config().app_id(), "test_app");
1521 }
1522
1523 #[cfg(feature = "im")]
1524 #[test]
1525 fn test_im_client_config() {
1526 let config = create_test_config();
1527 let client = CommunicationClient::new(config);
1528 assert_eq!(client.im.config().app_id(), "test_app");
1529 }
1530
1531 #[cfg(feature = "im")]
1532 #[test]
1533 fn test_message_recipient_constructors() {
1534 assert_eq!(
1535 MessageRecipient::open_id("ou_xxx"),
1536 MessageRecipient::new("ou_xxx", ReceiveIdType::OpenId)
1537 );
1538 assert_eq!(
1539 MessageRecipient::chat_id("oc_xxx"),
1540 MessageRecipient::new("oc_xxx", ReceiveIdType::ChatId)
1541 );
1542 }
1543
1544 #[cfg(feature = "im")]
1545 #[test]
1546 fn test_post_message_serialization() {
1547 let content = PostMessage::zh_cn("周报", "本周已完成 3 项任务")
1548 .into_content()
1549 .expect("post content should serialize");
1550
1551 let value: serde_json::Value =
1552 serde_json::from_str(&content).expect("content should be valid json");
1553 assert_eq!(value["post"]["zh_cn"]["title"], "周报");
1554 assert_eq!(
1555 value["post"]["zh_cn"]["content"][0][0]["text"],
1556 "本周已完成 3 项任务"
1557 );
1558 }
1559
1560 #[cfg(feature = "im")]
1561 #[test]
1562 fn test_reply_target_constructors() {
1563 assert_eq!(
1564 ReplyTarget::direct("om_xxx"),
1565 ReplyTarget {
1566 message_id: "om_xxx".to_string(),
1567 reply_in_thread: false,
1568 }
1569 );
1570 assert_eq!(
1571 ReplyTarget::in_thread("om_xxx"),
1572 ReplyTarget {
1573 message_id: "om_xxx".to_string(),
1574 reply_in_thread: true,
1575 }
1576 );
1577 }
1578
1579 #[cfg(feature = "im")]
1580 #[test]
1581 fn test_media_image_upload_defaults() {
1582 let upload = MediaImageUpload::new(vec![1, 2, 3]).file_name("image.png");
1583 assert_eq!(upload.image_type, ImageType::Message);
1584 assert_eq!(upload.file_name.as_deref(), Some("image.png"));
1585 assert_eq!(upload.bytes, vec![1, 2, 3]);
1586 }
1587
1588 #[cfg(feature = "im")]
1589 #[test]
1590 fn test_media_file_upload_infers_type() {
1591 let upload = MediaFileUpload::new("report.pdf", vec![1, 2, 3]).duration(15);
1592 assert_eq!(upload.file_type, "pdf");
1593 assert_eq!(upload.file_name, "report.pdf");
1594 assert_eq!(upload.duration, Some(15));
1595 }
1596
1597 #[cfg(feature = "im")]
1598 #[test]
1599 fn test_build_text_message_body() {
1600 let body = ImClient::build_text_body(MessageRecipient::open_id("ou_xxx"), "hello".into())
1601 .expect("text body should build");
1602 let request_body: CreateMessageBody = body.into();
1603 assert_eq!(request_body.msg_type, "text");
1604 assert_eq!(request_body.receive_id, "ou_xxx");
1605 assert_eq!(request_body.content, r#"{"text":"hello"}"#);
1606 }
1607
1608 #[cfg(feature = "im")]
1609 #[test]
1610 fn test_build_post_message_body() {
1611 let body = ImClient::build_post_body(
1612 MessageRecipient::chat_id("oc_xxx"),
1613 PostMessage::zh_cn("项目播报", "今天完成发布"),
1614 )
1615 .expect("post body should build");
1616 let request_body: CreateMessageBody = body.into();
1617 let value: serde_json::Value =
1618 serde_json::from_str(&request_body.content).expect("content should be valid json");
1619
1620 assert_eq!(request_body.msg_type, "post");
1621 assert_eq!(request_body.receive_id, "oc_xxx");
1622 assert_eq!(value["post"]["zh_cn"]["title"], "项目播报");
1623 }
1624
1625 #[cfg(feature = "im")]
1626 #[test]
1627 fn test_build_media_message_body_for_image() {
1628 let body = ImClient::build_media_body(
1629 MessageRecipient::open_id("ou_xxx"),
1630 "image",
1631 serde_json::json!({ "image_key": "img_xxx" }).to_string(),
1632 )
1633 .expect("image body should build");
1634 let request_body: CreateMessageBody = body.into();
1635 assert_eq!(request_body.msg_type, "image");
1636 assert_eq!(request_body.content, r#"{"image_key":"img_xxx"}"#);
1637 }
1638
1639 #[cfg(feature = "im")]
1640 #[test]
1641 fn test_build_media_message_body_for_file() {
1642 let body = ImClient::build_media_body(
1643 MessageRecipient::chat_id("oc_xxx"),
1644 "file",
1645 serde_json::json!({ "file_key": "file_xxx" }).to_string(),
1646 )
1647 .expect("file body should build");
1648 let request_body: CreateMessageBody = body.into();
1649 assert_eq!(request_body.msg_type, "file");
1650 assert_eq!(request_body.receive_id, "oc_xxx");
1651 assert_eq!(request_body.content, r#"{"file_key":"file_xxx"}"#);
1652 }
1653
1654 #[cfg(feature = "im")]
1655 #[test]
1656 fn test_build_reply_text_message_body() {
1657 let body = ImClient::build_reply_text_body(ReplyTarget::direct("om_xxx"), "收到".into())
1658 .expect("reply text body should build");
1659 let request_body: ReplyMessageBody = body.into();
1660 assert_eq!(request_body.msg_type, "text");
1661 assert_eq!(request_body.reply_in_thread, Some(false));
1662 assert_eq!(request_body.content, r#"{"text":"收到"}"#);
1663 }
1664
1665 #[cfg(feature = "im")]
1666 #[test]
1667 fn test_build_reply_post_message_body() {
1668 let body = ImClient::build_reply_post_body(
1669 ReplyTarget::in_thread("om_xxx"),
1670 PostMessage::zh_cn("进展", "线程内同步"),
1671 )
1672 .expect("reply post body should build");
1673 let request_body: ReplyMessageBody = body.into();
1674 let value: serde_json::Value =
1675 serde_json::from_str(&request_body.content).expect("content should be valid json");
1676
1677 assert_eq!(request_body.msg_type, "post");
1678 assert_eq!(request_body.reply_in_thread, Some(true));
1679 assert_eq!(value["post"]["zh_cn"]["title"], "进展");
1680 }
1681
1682 #[cfg(feature = "im")]
1683 #[tokio::test]
1684 async fn test_send_image_rejects_empty_key() {
1685 let client = CommunicationClient::new(create_test_config());
1686 let error = client
1687 .im
1688 .send_image(MessageRecipient::open_id("ou_xxx"), "")
1689 .await
1690 .expect_err("empty image_key should fail");
1691 assert!(error.to_string().contains("image_key"));
1692 }
1693
1694 #[cfg(feature = "im")]
1695 #[tokio::test]
1696 async fn test_send_file_rejects_empty_key() {
1697 let client = CommunicationClient::new(create_test_config());
1698 let error = client
1699 .im
1700 .send_file(MessageRecipient::chat_id("oc_xxx"), "")
1701 .await
1702 .expect_err("empty file_key should fail");
1703 assert!(error.to_string().contains("file_key"));
1704 }
1705
1706 #[cfg(feature = "im")]
1707 #[tokio::test]
1708 async fn test_upload_image_rejects_empty_bytes() {
1709 let client = CommunicationClient::new(create_test_config());
1710 let error = client
1711 .im
1712 .upload_image(MediaImageUpload::new(Vec::new()))
1713 .await
1714 .expect_err("empty image bytes should fail");
1715 assert!(error.to_string().contains("image"));
1716 }
1717
1718 #[cfg(feature = "contact")]
1719 #[test]
1720 fn test_user_lookup_response_deserializes() {
1721 let response: UserLookupResponse = serde_json::from_value(serde_json::json!({
1722 "has_more": true,
1723 "page_token": "token_1",
1724 "users": [
1725 {
1726 "name": "zhangsan",
1727 "open_id": "ou_xxx",
1728 "user_id": "u_xxx",
1729 "department_ids": ["od_1"]
1730 }
1731 ]
1732 }))
1733 .expect("user lookup response should deserialize");
1734
1735 assert!(response.has_more);
1736 assert_eq!(response.page_token.as_deref(), Some("token_1"));
1737 assert_eq!(response.users[0].name, "zhangsan");
1738 assert_eq!(response.users[0].open_id, "ou_xxx");
1739 }
1740
1741 #[cfg(feature = "contact")]
1742 #[test]
1743 fn test_find_unique_user_by_name_rejects_duplicates() {
1744 let users = vec![
1745 UserLookupItem {
1746 name: "zhangsan".to_string(),
1747 open_id: "ou_1".to_string(),
1748 user_id: None,
1749 department_ids: vec![],
1750 },
1751 UserLookupItem {
1752 name: "zhangsan".to_string(),
1753 open_id: "ou_2".to_string(),
1754 user_id: None,
1755 department_ids: vec![],
1756 },
1757 ];
1758
1759 let error =
1760 find_unique_user_by_name(&users, "zhangsan").expect_err("duplicate user should fail");
1761 assert!(error.to_string().contains("多个同名用户"));
1762 }
1763
1764 #[cfg(feature = "im")]
1765 #[test]
1766 fn test_chat_lookup_response_deserializes() {
1767 let response: ChatLookupResponse = serde_json::from_value(serde_json::json!({
1768 "has_more": false,
1769 "items": [
1770 {
1771 "chat_id": "oc_xxx",
1772 "name": "项目群",
1773 "description": "研发群",
1774 "owner_id": "ou_owner",
1775 "owner_id_type": "open_id",
1776 "external": false,
1777 "tenant_key": "tenant_key",
1778 "chat_status": "normal"
1779 }
1780 ]
1781 }))
1782 .expect("chat lookup response should deserialize");
1783
1784 assert!(!response.has_more);
1785 assert_eq!(response.items[0].chat_id, "oc_xxx");
1786 assert_eq!(response.items[0].name, "项目群");
1787 }
1788
1789 #[cfg(feature = "im")]
1790 #[test]
1791 fn test_find_unique_chat_by_name_rejects_duplicates() {
1792 let chats = vec![
1793 ChatLookupItem {
1794 chat_id: "oc_1".to_string(),
1795 name: "项目群".to_string(),
1796 description: None,
1797 owner_id: None,
1798 owner_id_type: None,
1799 external: false,
1800 tenant_key: None,
1801 chat_status: None,
1802 },
1803 ChatLookupItem {
1804 chat_id: "oc_2".to_string(),
1805 name: "项目群".to_string(),
1806 description: None,
1807 owner_id: None,
1808 owner_id_type: None,
1809 external: false,
1810 tenant_key: None,
1811 chat_status: None,
1812 },
1813 ];
1814
1815 let error =
1816 find_unique_chat_by_name(&chats, "项目群").expect_err("duplicate chat should fail");
1817 assert!(error.to_string().contains("多个同名群聊"));
1818 }
1819
1820 #[cfg(feature = "contact")]
1821 #[test]
1822 fn test_contact_client_config() {
1823 let config = create_test_config();
1824 let client = CommunicationClient::new(config);
1825 assert_eq!(client.contact.config().app_id(), "test_app");
1826 }
1827}