1use super::{
7 Channel, ChannelCapabilities, ChannelMessage, ChannelStatus, ChannelType, ChannelUser,
8 MessageId, StreamingMode,
9};
10use crate::error::{ChannelError, RustantError};
11use crate::oauth::AuthMethod;
12use crate::secret_ref::{SecretRef, SecretResolver};
13use async_trait::async_trait;
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18pub struct SlackConfig {
19 pub bot_token: SecretRef,
23 pub app_token: Option<String>,
24 pub default_channel: Option<String>,
25 pub allowed_channels: Vec<String>,
26 #[serde(default)]
29 pub auth_method: AuthMethod,
30}
31
32impl SlackConfig {
33 pub fn resolve_bot_token(&self) -> Result<String, crate::secret_ref::SecretResolveError> {
35 let store = crate::credentials::KeyringCredentialStore::new();
36 SecretResolver::resolve(&self.bot_token, &store)
37 }
38}
39
40#[derive(Debug, Clone)]
44pub struct SlackMessage {
45 pub ts: String,
46 pub channel: String,
47 pub user: String,
48 pub text: String,
49 pub thread_ts: Option<String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct SlackChannelInfo {
55 pub id: String,
56 pub name: String,
57 pub is_private: bool,
58 pub is_member: bool,
59 pub num_members: u64,
60 pub topic: String,
61 pub purpose: String,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct SlackUserInfo {
67 pub id: String,
68 pub name: String,
69 pub real_name: String,
70 pub display_name: String,
71 pub is_bot: bool,
72 pub is_admin: bool,
73 pub email: Option<String>,
74 pub status_text: String,
75 pub status_emoji: String,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct SlackReaction {
81 pub name: String,
82 pub count: u64,
83 pub users: Vec<String>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct SlackFile {
89 pub id: String,
90 pub name: String,
91 pub filetype: String,
92 pub size: u64,
93 pub url_private: String,
94 pub user: String,
95 pub timestamp: u64,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct SlackTeamInfo {
101 pub id: String,
102 pub name: String,
103 pub domain: String,
104 pub icon_url: Option<String>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct SlackUserGroup {
110 pub id: String,
111 pub name: String,
112 pub handle: String,
113 pub description: String,
114 pub user_count: u64,
115}
116
117#[async_trait]
121pub trait SlackHttpClient: Send + Sync {
122 async fn post_message(&self, channel: &str, text: &str) -> Result<String, String>;
124 async fn post_thread_reply(
125 &self,
126 channel: &str,
127 thread_ts: &str,
128 text: &str,
129 ) -> Result<String, String>;
130 async fn conversations_history(
131 &self,
132 channel: &str,
133 limit: usize,
134 ) -> Result<Vec<SlackMessage>, String>;
135 async fn auth_test(&self) -> Result<String, String>;
136
137 async fn conversations_list(
139 &self,
140 types: &str,
141 limit: usize,
142 ) -> Result<Vec<SlackChannelInfo>, String>;
143 async fn conversations_join(&self, channel_id: &str) -> Result<(), String>;
144 async fn conversations_info(&self, channel_id: &str) -> Result<SlackChannelInfo, String>;
145
146 async fn users_list(&self, limit: usize) -> Result<Vec<SlackUserInfo>, String>;
148 async fn users_info(&self, user_id: &str) -> Result<SlackUserInfo, String>;
149
150 async fn reactions_add(&self, channel: &str, timestamp: &str, name: &str)
152 -> Result<(), String>;
153 async fn reactions_get(
154 &self,
155 channel: &str,
156 timestamp: &str,
157 ) -> Result<Vec<SlackReaction>, String>;
158
159 async fn files_list(
161 &self,
162 channel: Option<&str>,
163 limit: usize,
164 ) -> Result<Vec<SlackFile>, String>;
165
166 async fn team_info(&self) -> Result<SlackTeamInfo, String>;
168 async fn usergroups_list(&self) -> Result<Vec<SlackUserGroup>, String>;
169
170 async fn conversations_open(&self, user_ids: &[&str]) -> Result<String, String>;
172}
173
174pub struct SlackChannel {
178 config: SlackConfig,
179 status: ChannelStatus,
180 http_client: Box<dyn SlackHttpClient>,
181 name: String,
182}
183
184impl SlackChannel {
185 pub fn new(config: SlackConfig, http_client: Box<dyn SlackHttpClient>) -> Self {
186 Self {
187 config,
188 status: ChannelStatus::Disconnected,
189 http_client,
190 name: "slack".to_string(),
191 }
192 }
193
194 pub fn with_name(mut self, name: impl Into<String>) -> Self {
195 self.name = name.into();
196 self
197 }
198
199 pub async fn list_channels(&self) -> Result<Vec<SlackChannelInfo>, RustantError> {
203 self.http_client
204 .conversations_list("public_channel,private_channel", 200)
205 .await
206 .map_err(|e| {
207 RustantError::Channel(ChannelError::ConnectionFailed {
208 name: self.name.clone(),
209 message: e,
210 })
211 })
212 }
213
214 pub async fn channel_info(&self, channel_id: &str) -> Result<SlackChannelInfo, RustantError> {
216 self.http_client
217 .conversations_info(channel_id)
218 .await
219 .map_err(|e| {
220 RustantError::Channel(ChannelError::ConnectionFailed {
221 name: self.name.clone(),
222 message: e,
223 })
224 })
225 }
226
227 pub async fn join_channel(&self, channel_id: &str) -> Result<(), RustantError> {
229 self.http_client
230 .conversations_join(channel_id)
231 .await
232 .map_err(|e| {
233 RustantError::Channel(ChannelError::ConnectionFailed {
234 name: self.name.clone(),
235 message: e,
236 })
237 })
238 }
239
240 pub async fn list_users(&self) -> Result<Vec<SlackUserInfo>, RustantError> {
242 self.http_client.users_list(200).await.map_err(|e| {
243 RustantError::Channel(ChannelError::ConnectionFailed {
244 name: self.name.clone(),
245 message: e,
246 })
247 })
248 }
249
250 pub async fn get_user_info(&self, user_id: &str) -> Result<SlackUserInfo, RustantError> {
252 self.http_client.users_info(user_id).await.map_err(|e| {
253 RustantError::Channel(ChannelError::ConnectionFailed {
254 name: self.name.clone(),
255 message: e,
256 })
257 })
258 }
259
260 pub async fn add_reaction(
262 &self,
263 channel: &str,
264 timestamp: &str,
265 emoji: &str,
266 ) -> Result<(), RustantError> {
267 self.http_client
268 .reactions_add(channel, timestamp, emoji)
269 .await
270 .map_err(|e| {
271 RustantError::Channel(ChannelError::SendFailed {
272 name: self.name.clone(),
273 message: e,
274 })
275 })
276 }
277
278 pub async fn get_reactions(
280 &self,
281 channel: &str,
282 timestamp: &str,
283 ) -> Result<Vec<SlackReaction>, RustantError> {
284 self.http_client
285 .reactions_get(channel, timestamp)
286 .await
287 .map_err(|e| {
288 RustantError::Channel(ChannelError::ConnectionFailed {
289 name: self.name.clone(),
290 message: e,
291 })
292 })
293 }
294
295 pub async fn list_files(&self, channel: Option<&str>) -> Result<Vec<SlackFile>, RustantError> {
297 self.http_client
298 .files_list(channel, 100)
299 .await
300 .map_err(|e| {
301 RustantError::Channel(ChannelError::ConnectionFailed {
302 name: self.name.clone(),
303 message: e,
304 })
305 })
306 }
307
308 pub async fn get_team_info(&self) -> Result<SlackTeamInfo, RustantError> {
310 self.http_client.team_info().await.map_err(|e| {
311 RustantError::Channel(ChannelError::ConnectionFailed {
312 name: self.name.clone(),
313 message: e,
314 })
315 })
316 }
317
318 pub async fn list_usergroups(&self) -> Result<Vec<SlackUserGroup>, RustantError> {
320 self.http_client.usergroups_list().await.map_err(|e| {
321 RustantError::Channel(ChannelError::ConnectionFailed {
322 name: self.name.clone(),
323 message: e,
324 })
325 })
326 }
327
328 pub async fn open_dm(&self, user_ids: &[&str]) -> Result<String, RustantError> {
330 self.http_client
331 .conversations_open(user_ids)
332 .await
333 .map_err(|e| {
334 RustantError::Channel(ChannelError::ConnectionFailed {
335 name: self.name.clone(),
336 message: e,
337 })
338 })
339 }
340
341 pub async fn reply_in_thread(
343 &self,
344 channel: &str,
345 thread_ts: &str,
346 text: &str,
347 ) -> Result<MessageId, RustantError> {
348 self.http_client
349 .post_thread_reply(channel, thread_ts, text)
350 .await
351 .map(MessageId::new)
352 .map_err(|e| {
353 RustantError::Channel(ChannelError::SendFailed {
354 name: self.name.clone(),
355 message: e,
356 })
357 })
358 }
359
360 pub async fn read_history(
362 &self,
363 channel: &str,
364 limit: usize,
365 ) -> Result<Vec<SlackMessage>, RustantError> {
366 self.http_client
367 .conversations_history(channel, limit)
368 .await
369 .map_err(|e| {
370 RustantError::Channel(ChannelError::ConnectionFailed {
371 name: self.name.clone(),
372 message: e,
373 })
374 })
375 }
376}
377
378#[async_trait]
379impl Channel for SlackChannel {
380 fn name(&self) -> &str {
381 &self.name
382 }
383
384 fn channel_type(&self) -> ChannelType {
385 ChannelType::Slack
386 }
387
388 async fn connect(&mut self) -> Result<(), RustantError> {
389 if self.config.bot_token.is_empty() {
390 return Err(RustantError::Channel(ChannelError::AuthFailed {
391 name: self.name.clone(),
392 }));
393 }
394 self.http_client.auth_test().await.map_err(|_e| {
395 RustantError::Channel(ChannelError::AuthFailed {
396 name: self.name.clone(),
397 })
398 })?;
399 self.status = ChannelStatus::Connected;
400 Ok(())
401 }
402
403 async fn disconnect(&mut self) -> Result<(), RustantError> {
404 self.status = ChannelStatus::Disconnected;
405 Ok(())
406 }
407
408 async fn send_message(&self, msg: ChannelMessage) -> Result<MessageId, RustantError> {
409 let text = msg.content.as_text().unwrap_or("");
410 let channel = if msg.channel_id.is_empty() {
411 self.config.default_channel.as_deref().unwrap_or("general")
412 } else {
413 &msg.channel_id
414 };
415
416 self.http_client
417 .post_message(channel, text)
418 .await
419 .map(MessageId::new)
420 .map_err(|e| {
421 RustantError::Channel(ChannelError::SendFailed {
422 name: self.name.clone(),
423 message: e,
424 })
425 })
426 }
427
428 async fn receive_messages(&self) -> Result<Vec<ChannelMessage>, RustantError> {
429 let channels = if self.config.allowed_channels.is_empty() {
430 vec![
431 self.config
432 .default_channel
433 .clone()
434 .unwrap_or_else(|| "general".to_string()),
435 ]
436 } else {
437 self.config.allowed_channels.clone()
438 };
439
440 let mut all = Vec::new();
441 for ch in &channels {
442 let slack_msgs = self
443 .http_client
444 .conversations_history(ch, 25)
445 .await
446 .map_err(|e| {
447 RustantError::Channel(ChannelError::ConnectionFailed {
448 name: self.name.clone(),
449 message: e,
450 })
451 })?;
452
453 for sm in slack_msgs {
454 let sender = ChannelUser::new(&sm.user, ChannelType::Slack);
455 let msg = ChannelMessage::text(ChannelType::Slack, &sm.channel, sender, &sm.text);
456 all.push(msg);
457 }
458 }
459 Ok(all)
460 }
461
462 fn status(&self) -> ChannelStatus {
463 self.status
464 }
465
466 fn capabilities(&self) -> ChannelCapabilities {
467 ChannelCapabilities {
468 supports_threads: true,
469 supports_reactions: true,
470 supports_files: true,
471 supports_voice: false,
472 supports_video: false,
473 max_message_length: Some(40000),
474 supports_editing: false,
475 supports_deletion: false,
476 }
477 }
478
479 fn streaming_mode(&self) -> StreamingMode {
480 StreamingMode::WebSocket
481 }
482}
483
484pub struct RealSlackHttp {
488 client: reqwest::Client,
489 bot_token: String,
490}
491
492impl RealSlackHttp {
493 pub fn new(bot_token: String) -> Self {
494 Self {
495 client: reqwest::Client::new(),
496 bot_token,
497 }
498 }
499
500 fn auth_header(&self) -> String {
501 format!("Bearer {}", self.bot_token)
502 }
503
504 async fn slack_get(&self, url: &str) -> Result<serde_json::Value, String> {
506 let resp = self
507 .client
508 .get(url)
509 .header("Authorization", self.auth_header())
510 .send()
511 .await
512 .map_err(|e| format!("HTTP request failed: {}", e))?;
513
514 let status = resp.status();
515 let body = resp
516 .text()
517 .await
518 .map_err(|e| format!("Failed to read response: {}", e))?;
519
520 if !status.is_success() {
521 return Err(format!("HTTP {}: {}", status, body));
522 }
523
524 let json: serde_json::Value =
525 serde_json::from_str(&body).map_err(|e| format!("Invalid JSON: {}", e))?;
526
527 if json.get("ok") != Some(&serde_json::Value::Bool(true)) {
528 let error = json
529 .get("error")
530 .and_then(|e| e.as_str())
531 .unwrap_or("unknown_error");
532 return Err(format!("Slack API error: {}", error));
533 }
534
535 Ok(json)
536 }
537
538 async fn slack_post(
540 &self,
541 url: &str,
542 body: &serde_json::Value,
543 ) -> Result<serde_json::Value, String> {
544 let resp = self
545 .client
546 .post(url)
547 .header("Authorization", self.auth_header())
548 .header("Content-Type", "application/json; charset=utf-8")
549 .json(body)
550 .send()
551 .await
552 .map_err(|e| format!("HTTP request failed: {}", e))?;
553
554 let status = resp.status();
555 let body_text = resp
556 .text()
557 .await
558 .map_err(|e| format!("Failed to read response: {}", e))?;
559
560 if !status.is_success() {
561 return Err(format!("HTTP {}: {}", status, body_text));
562 }
563
564 let json: serde_json::Value =
565 serde_json::from_str(&body_text).map_err(|e| format!("Invalid JSON: {}", e))?;
566
567 if json.get("ok") != Some(&serde_json::Value::Bool(true)) {
568 let error = json
569 .get("error")
570 .and_then(|e| e.as_str())
571 .unwrap_or("unknown_error");
572 return Err(format!("Slack API error: {}", error));
573 }
574
575 Ok(json)
576 }
577}
578
579#[async_trait]
580impl SlackHttpClient for RealSlackHttp {
581 async fn post_message(&self, channel: &str, text: &str) -> Result<String, String> {
582 let body = serde_json::json!({ "channel": channel, "text": text });
583 let json = self
584 .slack_post("https://slack.com/api/chat.postMessage", &body)
585 .await?;
586 json.get("ts")
587 .and_then(|ts| ts.as_str())
588 .map(|s| s.to_string())
589 .ok_or_else(|| "Missing 'ts' in response".to_string())
590 }
591
592 async fn post_thread_reply(
593 &self,
594 channel: &str,
595 thread_ts: &str,
596 text: &str,
597 ) -> Result<String, String> {
598 let body = serde_json::json!({ "channel": channel, "thread_ts": thread_ts, "text": text });
599 let json = self
600 .slack_post("https://slack.com/api/chat.postMessage", &body)
601 .await?;
602 json.get("ts")
603 .and_then(|ts| ts.as_str())
604 .map(|s| s.to_string())
605 .ok_or_else(|| "Missing 'ts' in response".to_string())
606 }
607
608 async fn conversations_history(
609 &self,
610 channel: &str,
611 limit: usize,
612 ) -> Result<Vec<SlackMessage>, String> {
613 let url = format!(
614 "https://slack.com/api/conversations.history?channel={}&limit={}",
615 channel, limit
616 );
617 let json = self.slack_get(&url).await?;
618
619 let messages = json
620 .get("messages")
621 .and_then(|m| m.as_array())
622 .map(|arr| {
623 arr.iter()
624 .filter_map(|msg| {
625 let ts = msg.get("ts")?.as_str()?.to_string();
626 let user = msg
627 .get("user")
628 .and_then(|u| u.as_str())
629 .unwrap_or("unknown")
630 .to_string();
631 let text = msg
632 .get("text")
633 .and_then(|t| t.as_str())
634 .unwrap_or("")
635 .to_string();
636 let thread_ts = msg
637 .get("thread_ts")
638 .and_then(|t| t.as_str())
639 .map(|s| s.to_string());
640 Some(SlackMessage {
641 ts,
642 channel: channel.to_string(),
643 user,
644 text,
645 thread_ts,
646 })
647 })
648 .collect()
649 })
650 .unwrap_or_default();
651
652 Ok(messages)
653 }
654
655 async fn auth_test(&self) -> Result<String, String> {
656 let json = self
657 .slack_post("https://slack.com/api/auth.test", &serde_json::json!({}))
658 .await?;
659 json.get("user_id")
660 .and_then(|u| u.as_str())
661 .map(|s| s.to_string())
662 .ok_or_else(|| "Missing 'user_id' in auth.test response".to_string())
663 }
664
665 async fn conversations_list(
666 &self,
667 types: &str,
668 limit: usize,
669 ) -> Result<Vec<SlackChannelInfo>, String> {
670 let url = format!(
671 "https://slack.com/api/conversations.list?types={}&limit={}&exclude_archived=true",
672 types, limit
673 );
674 let json = self.slack_get(&url).await?;
675
676 let channels = json
677 .get("channels")
678 .and_then(|c| c.as_array())
679 .map(|arr| {
680 arr.iter()
681 .filter_map(|ch| {
682 Some(SlackChannelInfo {
683 id: ch.get("id")?.as_str()?.to_string(),
684 name: ch.get("name")?.as_str()?.to_string(),
685 is_private: ch
686 .get("is_private")
687 .and_then(|v| v.as_bool())
688 .unwrap_or(false),
689 is_member: ch
690 .get("is_member")
691 .and_then(|v| v.as_bool())
692 .unwrap_or(false),
693 num_members: ch
694 .get("num_members")
695 .and_then(|v| v.as_u64())
696 .unwrap_or(0),
697 topic: ch
698 .get("topic")
699 .and_then(|t| t.get("value"))
700 .and_then(|v| v.as_str())
701 .unwrap_or("")
702 .to_string(),
703 purpose: ch
704 .get("purpose")
705 .and_then(|p| p.get("value"))
706 .and_then(|v| v.as_str())
707 .unwrap_or("")
708 .to_string(),
709 })
710 })
711 .collect()
712 })
713 .unwrap_or_default();
714
715 Ok(channels)
716 }
717
718 async fn conversations_join(&self, channel_id: &str) -> Result<(), String> {
719 let body = serde_json::json!({ "channel": channel_id });
720 self.slack_post("https://slack.com/api/conversations.join", &body)
721 .await?;
722 Ok(())
723 }
724
725 async fn conversations_info(&self, channel_id: &str) -> Result<SlackChannelInfo, String> {
726 let url = format!(
727 "https://slack.com/api/conversations.info?channel={}",
728 channel_id
729 );
730 let json = self.slack_get(&url).await?;
731
732 let ch = json.get("channel").ok_or("Missing 'channel' in response")?;
733 Ok(SlackChannelInfo {
734 id: ch
735 .get("id")
736 .and_then(|v| v.as_str())
737 .unwrap_or("")
738 .to_string(),
739 name: ch
740 .get("name")
741 .and_then(|v| v.as_str())
742 .unwrap_or("")
743 .to_string(),
744 is_private: ch
745 .get("is_private")
746 .and_then(|v| v.as_bool())
747 .unwrap_or(false),
748 is_member: ch
749 .get("is_member")
750 .and_then(|v| v.as_bool())
751 .unwrap_or(false),
752 num_members: ch.get("num_members").and_then(|v| v.as_u64()).unwrap_or(0),
753 topic: ch
754 .get("topic")
755 .and_then(|t| t.get("value"))
756 .and_then(|v| v.as_str())
757 .unwrap_or("")
758 .to_string(),
759 purpose: ch
760 .get("purpose")
761 .and_then(|p| p.get("value"))
762 .and_then(|v| v.as_str())
763 .unwrap_or("")
764 .to_string(),
765 })
766 }
767
768 async fn users_list(&self, limit: usize) -> Result<Vec<SlackUserInfo>, String> {
769 let url = format!("https://slack.com/api/users.list?limit={}", limit);
770 let json = self.slack_get(&url).await?;
771
772 let users = json
773 .get("members")
774 .and_then(|m| m.as_array())
775 .map(|arr| {
776 arr.iter()
777 .filter_map(|u| {
778 let profile = u.get("profile")?;
779 Some(SlackUserInfo {
780 id: u.get("id")?.as_str()?.to_string(),
781 name: u
782 .get("name")
783 .and_then(|v| v.as_str())
784 .unwrap_or("")
785 .to_string(),
786 real_name: profile
787 .get("real_name")
788 .and_then(|v| v.as_str())
789 .unwrap_or("")
790 .to_string(),
791 display_name: profile
792 .get("display_name")
793 .and_then(|v| v.as_str())
794 .unwrap_or("")
795 .to_string(),
796 is_bot: u.get("is_bot").and_then(|v| v.as_bool()).unwrap_or(false),
797 is_admin: u.get("is_admin").and_then(|v| v.as_bool()).unwrap_or(false),
798 email: profile
799 .get("email")
800 .and_then(|v| v.as_str())
801 .map(|s| s.to_string()),
802 status_text: profile
803 .get("status_text")
804 .and_then(|v| v.as_str())
805 .unwrap_or("")
806 .to_string(),
807 status_emoji: profile
808 .get("status_emoji")
809 .and_then(|v| v.as_str())
810 .unwrap_or("")
811 .to_string(),
812 })
813 })
814 .collect()
815 })
816 .unwrap_or_default();
817
818 Ok(users)
819 }
820
821 async fn users_info(&self, user_id: &str) -> Result<SlackUserInfo, String> {
822 let url = format!("https://slack.com/api/users.info?user={}", user_id);
823 let json = self.slack_get(&url).await?;
824
825 let u = json.get("user").ok_or("Missing 'user' in response")?;
826 let profile = u.get("profile").ok_or("Missing 'profile'")?;
827
828 Ok(SlackUserInfo {
829 id: u
830 .get("id")
831 .and_then(|v| v.as_str())
832 .unwrap_or("")
833 .to_string(),
834 name: u
835 .get("name")
836 .and_then(|v| v.as_str())
837 .unwrap_or("")
838 .to_string(),
839 real_name: profile
840 .get("real_name")
841 .and_then(|v| v.as_str())
842 .unwrap_or("")
843 .to_string(),
844 display_name: profile
845 .get("display_name")
846 .and_then(|v| v.as_str())
847 .unwrap_or("")
848 .to_string(),
849 is_bot: u.get("is_bot").and_then(|v| v.as_bool()).unwrap_or(false),
850 is_admin: u.get("is_admin").and_then(|v| v.as_bool()).unwrap_or(false),
851 email: profile
852 .get("email")
853 .and_then(|v| v.as_str())
854 .map(|s| s.to_string()),
855 status_text: profile
856 .get("status_text")
857 .and_then(|v| v.as_str())
858 .unwrap_or("")
859 .to_string(),
860 status_emoji: profile
861 .get("status_emoji")
862 .and_then(|v| v.as_str())
863 .unwrap_or("")
864 .to_string(),
865 })
866 }
867
868 async fn reactions_add(
869 &self,
870 channel: &str,
871 timestamp: &str,
872 name: &str,
873 ) -> Result<(), String> {
874 let body = serde_json::json!({ "channel": channel, "timestamp": timestamp, "name": name });
875 self.slack_post("https://slack.com/api/reactions.add", &body)
876 .await?;
877 Ok(())
878 }
879
880 async fn reactions_get(
881 &self,
882 channel: &str,
883 timestamp: &str,
884 ) -> Result<Vec<SlackReaction>, String> {
885 let url = format!(
886 "https://slack.com/api/reactions.get?channel={}×tamp={}&full=true",
887 channel, timestamp
888 );
889 let json = self.slack_get(&url).await?;
890
891 let reactions = json
892 .get("message")
893 .and_then(|m| m.get("reactions"))
894 .and_then(|r| r.as_array())
895 .map(|arr| {
896 arr.iter()
897 .filter_map(|r| {
898 Some(SlackReaction {
899 name: r.get("name")?.as_str()?.to_string(),
900 count: r.get("count").and_then(|c| c.as_u64()).unwrap_or(0),
901 users: r
902 .get("users")
903 .and_then(|u| u.as_array())
904 .map(|a| {
905 a.iter()
906 .filter_map(|v| v.as_str().map(|s| s.to_string()))
907 .collect()
908 })
909 .unwrap_or_default(),
910 })
911 })
912 .collect()
913 })
914 .unwrap_or_default();
915
916 Ok(reactions)
917 }
918
919 async fn files_list(
920 &self,
921 channel: Option<&str>,
922 limit: usize,
923 ) -> Result<Vec<SlackFile>, String> {
924 let mut url = format!("https://slack.com/api/files.list?count={}", limit);
925 if let Some(ch) = channel {
926 url.push_str(&format!("&channel={}", ch));
927 }
928
929 let json = self.slack_get(&url).await?;
930
931 let files = json
932 .get("files")
933 .and_then(|f| f.as_array())
934 .map(|arr| {
935 arr.iter()
936 .filter_map(|f| {
937 Some(SlackFile {
938 id: f.get("id")?.as_str()?.to_string(),
939 name: f
940 .get("name")
941 .and_then(|v| v.as_str())
942 .unwrap_or("unnamed")
943 .to_string(),
944 filetype: f
945 .get("filetype")
946 .and_then(|v| v.as_str())
947 .unwrap_or("")
948 .to_string(),
949 size: f.get("size").and_then(|v| v.as_u64()).unwrap_or(0),
950 url_private: f
951 .get("url_private")
952 .and_then(|v| v.as_str())
953 .unwrap_or("")
954 .to_string(),
955 user: f
956 .get("user")
957 .and_then(|v| v.as_str())
958 .unwrap_or("")
959 .to_string(),
960 timestamp: f.get("timestamp").and_then(|v| v.as_u64()).unwrap_or(0),
961 })
962 })
963 .collect()
964 })
965 .unwrap_or_default();
966
967 Ok(files)
968 }
969
970 async fn team_info(&self) -> Result<SlackTeamInfo, String> {
971 let json = self.slack_get("https://slack.com/api/team.info").await?;
972
973 let team = json.get("team").ok_or("Missing 'team' in response")?;
974 Ok(SlackTeamInfo {
975 id: team
976 .get("id")
977 .and_then(|v| v.as_str())
978 .unwrap_or("")
979 .to_string(),
980 name: team
981 .get("name")
982 .and_then(|v| v.as_str())
983 .unwrap_or("")
984 .to_string(),
985 domain: team
986 .get("domain")
987 .and_then(|v| v.as_str())
988 .unwrap_or("")
989 .to_string(),
990 icon_url: team
991 .get("icon")
992 .and_then(|i| i.get("image_132"))
993 .and_then(|v| v.as_str())
994 .map(|s| s.to_string()),
995 })
996 }
997
998 async fn usergroups_list(&self) -> Result<Vec<SlackUserGroup>, String> {
999 let json = self
1000 .slack_get("https://slack.com/api/usergroups.list?include_count=true")
1001 .await?;
1002
1003 let groups = json
1004 .get("usergroups")
1005 .and_then(|g| g.as_array())
1006 .map(|arr| {
1007 arr.iter()
1008 .filter_map(|g| {
1009 Some(SlackUserGroup {
1010 id: g.get("id")?.as_str()?.to_string(),
1011 name: g
1012 .get("name")
1013 .and_then(|v| v.as_str())
1014 .unwrap_or("")
1015 .to_string(),
1016 handle: g
1017 .get("handle")
1018 .and_then(|v| v.as_str())
1019 .unwrap_or("")
1020 .to_string(),
1021 description: g
1022 .get("description")
1023 .and_then(|v| v.as_str())
1024 .unwrap_or("")
1025 .to_string(),
1026 user_count: g.get("user_count").and_then(|v| v.as_u64()).unwrap_or(0),
1027 })
1028 })
1029 .collect()
1030 })
1031 .unwrap_or_default();
1032
1033 Ok(groups)
1034 }
1035
1036 async fn conversations_open(&self, user_ids: &[&str]) -> Result<String, String> {
1037 let body = serde_json::json!({ "users": user_ids.join(",") });
1038 let json = self
1039 .slack_post("https://slack.com/api/conversations.open", &body)
1040 .await?;
1041
1042 json.get("channel")
1043 .and_then(|c| c.get("id"))
1044 .and_then(|id| id.as_str())
1045 .map(|s| s.to_string())
1046 .ok_or_else(|| "Missing 'channel.id' in response".to_string())
1047 }
1048}
1049
1050pub fn create_slack_channel(config: SlackConfig) -> SlackChannel {
1052 let resolved_token = config.resolve_bot_token().unwrap_or_else(|e| {
1053 tracing::warn!(
1054 "Failed to resolve Slack bot token: {}. Falling back to raw value.",
1055 e
1056 );
1057 config.bot_token.as_str().to_string()
1058 });
1059 let http = RealSlackHttp::new(resolved_token);
1060 SlackChannel::new(config, Box::new(http))
1061}
1062
1063#[cfg(test)]
1066mod tests {
1067 use super::*;
1068 use crate::channels::ChannelUser;
1069 use std::sync::{Arc, Mutex};
1070
1071 struct MockSlackHttp {
1072 sent: Arc<Mutex<Vec<(String, String)>>>,
1073 messages: Vec<SlackMessage>,
1074 auth_ok: bool,
1075 }
1076
1077 impl MockSlackHttp {
1078 fn new() -> Self {
1079 Self {
1080 sent: Arc::new(Mutex::new(Vec::new())),
1081 messages: Vec::new(),
1082 auth_ok: true,
1083 }
1084 }
1085
1086 fn with_messages(mut self, messages: Vec<SlackMessage>) -> Self {
1087 self.messages = messages;
1088 self
1089 }
1090 }
1091
1092 #[async_trait]
1093 impl SlackHttpClient for MockSlackHttp {
1094 async fn post_message(&self, channel: &str, text: &str) -> Result<String, String> {
1095 self.sent
1096 .lock()
1097 .unwrap()
1098 .push((channel.to_string(), text.to_string()));
1099 Ok("1234567890.123456".to_string())
1100 }
1101
1102 async fn post_thread_reply(
1103 &self,
1104 channel: &str,
1105 _thread_ts: &str,
1106 text: &str,
1107 ) -> Result<String, String> {
1108 self.sent
1109 .lock()
1110 .unwrap()
1111 .push((channel.to_string(), text.to_string()));
1112 Ok("1234567890.654321".to_string())
1113 }
1114
1115 async fn conversations_history(
1116 &self,
1117 _channel: &str,
1118 _limit: usize,
1119 ) -> Result<Vec<SlackMessage>, String> {
1120 Ok(self.messages.clone())
1121 }
1122
1123 async fn auth_test(&self) -> Result<String, String> {
1124 if self.auth_ok {
1125 Ok("bot-user-id".to_string())
1126 } else {
1127 Err("invalid_auth".to_string())
1128 }
1129 }
1130
1131 async fn conversations_list(
1132 &self,
1133 _types: &str,
1134 _limit: usize,
1135 ) -> Result<Vec<SlackChannelInfo>, String> {
1136 Ok(vec![
1137 SlackChannelInfo {
1138 id: "C001".into(),
1139 name: "general".into(),
1140 is_private: false,
1141 is_member: true,
1142 num_members: 42,
1143 topic: "General chat".into(),
1144 purpose: "Company-wide".into(),
1145 },
1146 SlackChannelInfo {
1147 id: "C002".into(),
1148 name: "random".into(),
1149 is_private: false,
1150 is_member: true,
1151 num_members: 38,
1152 topic: "".into(),
1153 purpose: "Random stuff".into(),
1154 },
1155 ])
1156 }
1157
1158 async fn conversations_join(&self, _channel_id: &str) -> Result<(), String> {
1159 Ok(())
1160 }
1161
1162 async fn conversations_info(&self, channel_id: &str) -> Result<SlackChannelInfo, String> {
1163 Ok(SlackChannelInfo {
1164 id: channel_id.to_string(),
1165 name: "general".into(),
1166 is_private: false,
1167 is_member: true,
1168 num_members: 42,
1169 topic: "General chat".into(),
1170 purpose: "Company-wide".into(),
1171 })
1172 }
1173
1174 async fn users_list(&self, _limit: usize) -> Result<Vec<SlackUserInfo>, String> {
1175 Ok(vec![SlackUserInfo {
1176 id: "U001".into(),
1177 name: "alice".into(),
1178 real_name: "Alice Smith".into(),
1179 display_name: "alice".into(),
1180 is_bot: false,
1181 is_admin: true,
1182 email: Some("alice@example.com".into()),
1183 status_text: "Working".into(),
1184 status_emoji: ":computer:".into(),
1185 }])
1186 }
1187
1188 async fn users_info(&self, user_id: &str) -> Result<SlackUserInfo, String> {
1189 Ok(SlackUserInfo {
1190 id: user_id.to_string(),
1191 name: "alice".into(),
1192 real_name: "Alice Smith".into(),
1193 display_name: "alice".into(),
1194 is_bot: false,
1195 is_admin: true,
1196 email: Some("alice@example.com".into()),
1197 status_text: "Working".into(),
1198 status_emoji: ":computer:".into(),
1199 })
1200 }
1201
1202 async fn reactions_add(
1203 &self,
1204 _channel: &str,
1205 _timestamp: &str,
1206 _name: &str,
1207 ) -> Result<(), String> {
1208 Ok(())
1209 }
1210
1211 async fn reactions_get(
1212 &self,
1213 _channel: &str,
1214 _timestamp: &str,
1215 ) -> Result<Vec<SlackReaction>, String> {
1216 Ok(vec![SlackReaction {
1217 name: "thumbsup".into(),
1218 count: 3,
1219 users: vec!["U001".into(), "U002".into(), "U003".into()],
1220 }])
1221 }
1222
1223 async fn files_list(
1224 &self,
1225 _channel: Option<&str>,
1226 _limit: usize,
1227 ) -> Result<Vec<SlackFile>, String> {
1228 Ok(vec![SlackFile {
1229 id: "F001".into(),
1230 name: "report.pdf".into(),
1231 filetype: "pdf".into(),
1232 size: 1024,
1233 url_private: "https://files.slack.com/report.pdf".into(),
1234 user: "U001".into(),
1235 timestamp: 1700000000,
1236 }])
1237 }
1238
1239 async fn team_info(&self) -> Result<SlackTeamInfo, String> {
1240 Ok(SlackTeamInfo {
1241 id: "T001".into(),
1242 name: "Test Workspace".into(),
1243 domain: "test-workspace".into(),
1244 icon_url: None,
1245 })
1246 }
1247
1248 async fn usergroups_list(&self) -> Result<Vec<SlackUserGroup>, String> {
1249 Ok(vec![SlackUserGroup {
1250 id: "S001".into(),
1251 name: "Engineering".into(),
1252 handle: "engineering".into(),
1253 description: "Engineering team".into(),
1254 user_count: 15,
1255 }])
1256 }
1257
1258 async fn conversations_open(&self, _user_ids: &[&str]) -> Result<String, String> {
1259 Ok("D001".to_string())
1260 }
1261 }
1262
1263 #[tokio::test]
1266 async fn test_slack_connect() {
1267 let config = SlackConfig {
1268 bot_token: "xoxb-123".into(),
1269 ..Default::default()
1270 };
1271 let mut ch = SlackChannel::new(config, Box::new(MockSlackHttp::new()));
1272 ch.connect().await.unwrap();
1273 assert!(ch.is_connected());
1274 }
1275
1276 #[tokio::test]
1277 async fn test_slack_send_message() {
1278 let config = SlackConfig {
1279 bot_token: "xoxb-123".into(),
1280 default_channel: Some("general".into()),
1281 ..Default::default()
1282 };
1283 let http = MockSlackHttp::new();
1284 let sent = http.sent.clone();
1285 let mut ch = SlackChannel::new(config, Box::new(http));
1286 ch.connect().await.unwrap();
1287
1288 let sender = ChannelUser::new("bot", ChannelType::Slack);
1289 let msg = ChannelMessage::text(ChannelType::Slack, "random", sender, "Hello Slack!");
1290 ch.send_message(msg).await.unwrap();
1291
1292 let sent = sent.lock().unwrap();
1293 assert_eq!(sent[0].0, "random");
1294 assert_eq!(sent[0].1, "Hello Slack!");
1295 }
1296
1297 #[tokio::test]
1298 async fn test_slack_receive_messages() {
1299 let config = SlackConfig {
1300 bot_token: "xoxb-123".into(),
1301 allowed_channels: vec!["general".into()],
1302 ..Default::default()
1303 };
1304 let http = MockSlackHttp::new().with_messages(vec![SlackMessage {
1305 ts: "123.456".into(),
1306 channel: "general".into(),
1307 user: "U123".into(),
1308 text: "hey".into(),
1309 thread_ts: None,
1310 }]);
1311 let mut ch = SlackChannel::new(config, Box::new(http));
1312 ch.connect().await.unwrap();
1313
1314 let msgs = ch.receive_messages().await.unwrap();
1315 assert_eq!(msgs.len(), 1);
1316 assert_eq!(msgs[0].content.as_text(), Some("hey"));
1317 }
1318
1319 #[test]
1320 fn test_slack_capabilities() {
1321 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1322 let caps = ch.capabilities();
1323 assert!(caps.supports_threads);
1324 assert!(caps.supports_reactions);
1325 assert!(caps.supports_files);
1326 assert!(!caps.supports_voice);
1327 assert_eq!(caps.max_message_length, Some(40000));
1328 }
1329
1330 #[test]
1331 fn test_slack_streaming_mode() {
1332 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1333 assert_eq!(ch.streaming_mode(), StreamingMode::WebSocket);
1334 }
1335
1336 #[tokio::test]
1337 async fn test_slack_oauth_config_connect() {
1338 let config = SlackConfig {
1339 bot_token: "xoxb-oauth-token-from-oauth-flow".into(),
1340 auth_method: AuthMethod::OAuth,
1341 ..Default::default()
1342 };
1343 let mut ch = SlackChannel::new(config, Box::new(MockSlackHttp::new()));
1344 ch.connect().await.unwrap();
1345 assert!(ch.is_connected());
1346 }
1347
1348 #[test]
1349 fn test_slack_config_auth_method_default() {
1350 let config = SlackConfig::default();
1351 assert_eq!(config.auth_method, AuthMethod::ApiKey);
1352 }
1353
1354 #[test]
1355 fn test_slack_config_auth_method_serde() {
1356 let config = SlackConfig {
1357 bot_token: "xoxb-test".into(),
1358 auth_method: AuthMethod::OAuth,
1359 ..Default::default()
1360 };
1361 let json = serde_json::to_string(&config).unwrap();
1362 assert!(json.contains("\"oauth\""));
1363 let parsed: SlackConfig = serde_json::from_str(&json).unwrap();
1364 assert_eq!(parsed.auth_method, AuthMethod::OAuth);
1365 }
1366
1367 #[tokio::test]
1370 async fn test_slack_list_channels() {
1371 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1372 let channels = ch.list_channels().await.unwrap();
1373 assert_eq!(channels.len(), 2);
1374 assert_eq!(channels[0].name, "general");
1375 assert_eq!(channels[1].name, "random");
1376 assert_eq!(channels[0].num_members, 42);
1377 }
1378
1379 #[tokio::test]
1380 async fn test_slack_channel_info() {
1381 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1382 let info = ch.channel_info("C001").await.unwrap();
1383 assert_eq!(info.id, "C001");
1384 assert_eq!(info.name, "general");
1385 }
1386
1387 #[tokio::test]
1388 async fn test_slack_join_channel() {
1389 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1390 ch.join_channel("C002").await.unwrap();
1391 }
1392
1393 #[tokio::test]
1394 async fn test_slack_list_users() {
1395 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1396 let users = ch.list_users().await.unwrap();
1397 assert_eq!(users.len(), 1);
1398 assert_eq!(users[0].name, "alice");
1399 assert_eq!(users[0].real_name, "Alice Smith");
1400 assert!(users[0].is_admin);
1401 assert!(!users[0].is_bot);
1402 }
1403
1404 #[tokio::test]
1405 async fn test_slack_get_user_info() {
1406 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1407 let user = ch.get_user_info("U001").await.unwrap();
1408 assert_eq!(user.id, "U001");
1409 assert_eq!(user.email, Some("alice@example.com".into()));
1410 }
1411
1412 #[tokio::test]
1413 async fn test_slack_add_reaction() {
1414 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1415 ch.add_reaction("C001", "123.456", "thumbsup")
1416 .await
1417 .unwrap();
1418 }
1419
1420 #[tokio::test]
1421 async fn test_slack_get_reactions() {
1422 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1423 let reactions = ch.get_reactions("C001", "123.456").await.unwrap();
1424 assert_eq!(reactions.len(), 1);
1425 assert_eq!(reactions[0].name, "thumbsup");
1426 assert_eq!(reactions[0].count, 3);
1427 assert_eq!(reactions[0].users.len(), 3);
1428 }
1429
1430 #[tokio::test]
1431 async fn test_slack_list_files() {
1432 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1433 let files = ch.list_files(None).await.unwrap();
1434 assert_eq!(files.len(), 1);
1435 assert_eq!(files[0].name, "report.pdf");
1436 assert_eq!(files[0].size, 1024);
1437 }
1438
1439 #[tokio::test]
1440 async fn test_slack_team_info() {
1441 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1442 let team = ch.get_team_info().await.unwrap();
1443 assert_eq!(team.name, "Test Workspace");
1444 assert_eq!(team.domain, "test-workspace");
1445 }
1446
1447 #[tokio::test]
1448 async fn test_slack_list_usergroups() {
1449 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1450 let groups = ch.list_usergroups().await.unwrap();
1451 assert_eq!(groups.len(), 1);
1452 assert_eq!(groups[0].handle, "engineering");
1453 assert_eq!(groups[0].user_count, 15);
1454 }
1455
1456 #[tokio::test]
1457 async fn test_slack_open_dm() {
1458 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1459 let dm_channel = ch.open_dm(&["U001"]).await.unwrap();
1460 assert_eq!(dm_channel, "D001");
1461 }
1462
1463 #[tokio::test]
1464 async fn test_slack_thread_reply() {
1465 let http = MockSlackHttp::new();
1466 let sent = http.sent.clone();
1467 let ch = SlackChannel::new(SlackConfig::default(), Box::new(http));
1468 let id = ch
1469 .reply_in_thread("C001", "123.456", "reply text")
1470 .await
1471 .unwrap();
1472 assert!(!id.0.is_empty());
1473 let sent = sent.lock().unwrap();
1474 assert_eq!(sent[0].1, "reply text");
1475 }
1476
1477 #[tokio::test]
1478 async fn test_slack_read_history() {
1479 let http = MockSlackHttp::new().with_messages(vec![SlackMessage {
1480 ts: "999.888".into(),
1481 channel: "test".into(),
1482 user: "U001".into(),
1483 text: "history msg".into(),
1484 thread_ts: None,
1485 }]);
1486 let ch = SlackChannel::new(SlackConfig::default(), Box::new(http));
1487 let msgs = ch.read_history("test", 10).await.unwrap();
1488 assert_eq!(msgs.len(), 1);
1489 assert_eq!(msgs[0].text, "history msg");
1490 }
1491}