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![self
431 .config
432 .default_channel
433 .clone()
434 .unwrap_or_else(|| "general".to_string())]
435 } else {
436 self.config.allowed_channels.clone()
437 };
438
439 let mut all = Vec::new();
440 for ch in &channels {
441 let slack_msgs = self
442 .http_client
443 .conversations_history(ch, 25)
444 .await
445 .map_err(|e| {
446 RustantError::Channel(ChannelError::ConnectionFailed {
447 name: self.name.clone(),
448 message: e,
449 })
450 })?;
451
452 for sm in slack_msgs {
453 let sender = ChannelUser::new(&sm.user, ChannelType::Slack);
454 let msg = ChannelMessage::text(ChannelType::Slack, &sm.channel, sender, &sm.text);
455 all.push(msg);
456 }
457 }
458 Ok(all)
459 }
460
461 fn status(&self) -> ChannelStatus {
462 self.status
463 }
464
465 fn capabilities(&self) -> ChannelCapabilities {
466 ChannelCapabilities {
467 supports_threads: true,
468 supports_reactions: true,
469 supports_files: true,
470 supports_voice: false,
471 supports_video: false,
472 max_message_length: Some(40000),
473 supports_editing: false,
474 supports_deletion: false,
475 }
476 }
477
478 fn streaming_mode(&self) -> StreamingMode {
479 StreamingMode::WebSocket
480 }
481}
482
483pub struct RealSlackHttp {
487 client: reqwest::Client,
488 bot_token: String,
489}
490
491impl RealSlackHttp {
492 pub fn new(bot_token: String) -> Self {
493 Self {
494 client: reqwest::Client::new(),
495 bot_token,
496 }
497 }
498
499 fn auth_header(&self) -> String {
500 format!("Bearer {}", self.bot_token)
501 }
502
503 async fn slack_get(&self, url: &str) -> Result<serde_json::Value, String> {
505 let resp = self
506 .client
507 .get(url)
508 .header("Authorization", self.auth_header())
509 .send()
510 .await
511 .map_err(|e| format!("HTTP request failed: {}", e))?;
512
513 let status = resp.status();
514 let body = resp
515 .text()
516 .await
517 .map_err(|e| format!("Failed to read response: {}", e))?;
518
519 if !status.is_success() {
520 return Err(format!("HTTP {}: {}", status, body));
521 }
522
523 let json: serde_json::Value =
524 serde_json::from_str(&body).map_err(|e| format!("Invalid JSON: {}", e))?;
525
526 if json.get("ok") != Some(&serde_json::Value::Bool(true)) {
527 let error = json
528 .get("error")
529 .and_then(|e| e.as_str())
530 .unwrap_or("unknown_error");
531 return Err(format!("Slack API error: {}", error));
532 }
533
534 Ok(json)
535 }
536
537 async fn slack_post(
539 &self,
540 url: &str,
541 body: &serde_json::Value,
542 ) -> Result<serde_json::Value, String> {
543 let resp = self
544 .client
545 .post(url)
546 .header("Authorization", self.auth_header())
547 .header("Content-Type", "application/json; charset=utf-8")
548 .json(body)
549 .send()
550 .await
551 .map_err(|e| format!("HTTP request failed: {}", e))?;
552
553 let status = resp.status();
554 let body_text = resp
555 .text()
556 .await
557 .map_err(|e| format!("Failed to read response: {}", e))?;
558
559 if !status.is_success() {
560 return Err(format!("HTTP {}: {}", status, body_text));
561 }
562
563 let json: serde_json::Value =
564 serde_json::from_str(&body_text).map_err(|e| format!("Invalid JSON: {}", e))?;
565
566 if json.get("ok") != Some(&serde_json::Value::Bool(true)) {
567 let error = json
568 .get("error")
569 .and_then(|e| e.as_str())
570 .unwrap_or("unknown_error");
571 return Err(format!("Slack API error: {}", error));
572 }
573
574 Ok(json)
575 }
576}
577
578#[async_trait]
579impl SlackHttpClient for RealSlackHttp {
580 async fn post_message(&self, channel: &str, text: &str) -> Result<String, String> {
581 let body = serde_json::json!({ "channel": channel, "text": text });
582 let json = self
583 .slack_post("https://slack.com/api/chat.postMessage", &body)
584 .await?;
585 json.get("ts")
586 .and_then(|ts| ts.as_str())
587 .map(|s| s.to_string())
588 .ok_or_else(|| "Missing 'ts' in response".to_string())
589 }
590
591 async fn post_thread_reply(
592 &self,
593 channel: &str,
594 thread_ts: &str,
595 text: &str,
596 ) -> Result<String, String> {
597 let body = serde_json::json!({ "channel": channel, "thread_ts": thread_ts, "text": text });
598 let json = self
599 .slack_post("https://slack.com/api/chat.postMessage", &body)
600 .await?;
601 json.get("ts")
602 .and_then(|ts| ts.as_str())
603 .map(|s| s.to_string())
604 .ok_or_else(|| "Missing 'ts' in response".to_string())
605 }
606
607 async fn conversations_history(
608 &self,
609 channel: &str,
610 limit: usize,
611 ) -> Result<Vec<SlackMessage>, String> {
612 let url = format!(
613 "https://slack.com/api/conversations.history?channel={}&limit={}",
614 channel, limit
615 );
616 let json = self.slack_get(&url).await?;
617
618 let messages = json
619 .get("messages")
620 .and_then(|m| m.as_array())
621 .map(|arr| {
622 arr.iter()
623 .filter_map(|msg| {
624 let ts = msg.get("ts")?.as_str()?.to_string();
625 let user = msg
626 .get("user")
627 .and_then(|u| u.as_str())
628 .unwrap_or("unknown")
629 .to_string();
630 let text = msg
631 .get("text")
632 .and_then(|t| t.as_str())
633 .unwrap_or("")
634 .to_string();
635 let thread_ts = msg
636 .get("thread_ts")
637 .and_then(|t| t.as_str())
638 .map(|s| s.to_string());
639 Some(SlackMessage {
640 ts,
641 channel: channel.to_string(),
642 user,
643 text,
644 thread_ts,
645 })
646 })
647 .collect()
648 })
649 .unwrap_or_default();
650
651 Ok(messages)
652 }
653
654 async fn auth_test(&self) -> Result<String, String> {
655 let json = self
656 .slack_post("https://slack.com/api/auth.test", &serde_json::json!({}))
657 .await?;
658 json.get("user_id")
659 .and_then(|u| u.as_str())
660 .map(|s| s.to_string())
661 .ok_or_else(|| "Missing 'user_id' in auth.test response".to_string())
662 }
663
664 async fn conversations_list(
665 &self,
666 types: &str,
667 limit: usize,
668 ) -> Result<Vec<SlackChannelInfo>, String> {
669 let url = format!(
670 "https://slack.com/api/conversations.list?types={}&limit={}&exclude_archived=true",
671 types, limit
672 );
673 let json = self.slack_get(&url).await?;
674
675 let channels = json
676 .get("channels")
677 .and_then(|c| c.as_array())
678 .map(|arr| {
679 arr.iter()
680 .filter_map(|ch| {
681 Some(SlackChannelInfo {
682 id: ch.get("id")?.as_str()?.to_string(),
683 name: ch.get("name")?.as_str()?.to_string(),
684 is_private: ch
685 .get("is_private")
686 .and_then(|v| v.as_bool())
687 .unwrap_or(false),
688 is_member: ch
689 .get("is_member")
690 .and_then(|v| v.as_bool())
691 .unwrap_or(false),
692 num_members: ch
693 .get("num_members")
694 .and_then(|v| v.as_u64())
695 .unwrap_or(0),
696 topic: ch
697 .get("topic")
698 .and_then(|t| t.get("value"))
699 .and_then(|v| v.as_str())
700 .unwrap_or("")
701 .to_string(),
702 purpose: ch
703 .get("purpose")
704 .and_then(|p| p.get("value"))
705 .and_then(|v| v.as_str())
706 .unwrap_or("")
707 .to_string(),
708 })
709 })
710 .collect()
711 })
712 .unwrap_or_default();
713
714 Ok(channels)
715 }
716
717 async fn conversations_join(&self, channel_id: &str) -> Result<(), String> {
718 let body = serde_json::json!({ "channel": channel_id });
719 self.slack_post("https://slack.com/api/conversations.join", &body)
720 .await?;
721 Ok(())
722 }
723
724 async fn conversations_info(&self, channel_id: &str) -> Result<SlackChannelInfo, String> {
725 let url = format!(
726 "https://slack.com/api/conversations.info?channel={}",
727 channel_id
728 );
729 let json = self.slack_get(&url).await?;
730
731 let ch = json.get("channel").ok_or("Missing 'channel' in response")?;
732 Ok(SlackChannelInfo {
733 id: ch
734 .get("id")
735 .and_then(|v| v.as_str())
736 .unwrap_or("")
737 .to_string(),
738 name: ch
739 .get("name")
740 .and_then(|v| v.as_str())
741 .unwrap_or("")
742 .to_string(),
743 is_private: ch
744 .get("is_private")
745 .and_then(|v| v.as_bool())
746 .unwrap_or(false),
747 is_member: ch
748 .get("is_member")
749 .and_then(|v| v.as_bool())
750 .unwrap_or(false),
751 num_members: ch.get("num_members").and_then(|v| v.as_u64()).unwrap_or(0),
752 topic: ch
753 .get("topic")
754 .and_then(|t| t.get("value"))
755 .and_then(|v| v.as_str())
756 .unwrap_or("")
757 .to_string(),
758 purpose: ch
759 .get("purpose")
760 .and_then(|p| p.get("value"))
761 .and_then(|v| v.as_str())
762 .unwrap_or("")
763 .to_string(),
764 })
765 }
766
767 async fn users_list(&self, limit: usize) -> Result<Vec<SlackUserInfo>, String> {
768 let url = format!("https://slack.com/api/users.list?limit={}", limit);
769 let json = self.slack_get(&url).await?;
770
771 let users = json
772 .get("members")
773 .and_then(|m| m.as_array())
774 .map(|arr| {
775 arr.iter()
776 .filter_map(|u| {
777 let profile = u.get("profile")?;
778 Some(SlackUserInfo {
779 id: u.get("id")?.as_str()?.to_string(),
780 name: u
781 .get("name")
782 .and_then(|v| v.as_str())
783 .unwrap_or("")
784 .to_string(),
785 real_name: profile
786 .get("real_name")
787 .and_then(|v| v.as_str())
788 .unwrap_or("")
789 .to_string(),
790 display_name: profile
791 .get("display_name")
792 .and_then(|v| v.as_str())
793 .unwrap_or("")
794 .to_string(),
795 is_bot: u.get("is_bot").and_then(|v| v.as_bool()).unwrap_or(false),
796 is_admin: u.get("is_admin").and_then(|v| v.as_bool()).unwrap_or(false),
797 email: profile
798 .get("email")
799 .and_then(|v| v.as_str())
800 .map(|s| s.to_string()),
801 status_text: profile
802 .get("status_text")
803 .and_then(|v| v.as_str())
804 .unwrap_or("")
805 .to_string(),
806 status_emoji: profile
807 .get("status_emoji")
808 .and_then(|v| v.as_str())
809 .unwrap_or("")
810 .to_string(),
811 })
812 })
813 .collect()
814 })
815 .unwrap_or_default();
816
817 Ok(users)
818 }
819
820 async fn users_info(&self, user_id: &str) -> Result<SlackUserInfo, String> {
821 let url = format!("https://slack.com/api/users.info?user={}", user_id);
822 let json = self.slack_get(&url).await?;
823
824 let u = json.get("user").ok_or("Missing 'user' in response")?;
825 let profile = u.get("profile").ok_or("Missing 'profile'")?;
826
827 Ok(SlackUserInfo {
828 id: u
829 .get("id")
830 .and_then(|v| v.as_str())
831 .unwrap_or("")
832 .to_string(),
833 name: u
834 .get("name")
835 .and_then(|v| v.as_str())
836 .unwrap_or("")
837 .to_string(),
838 real_name: profile
839 .get("real_name")
840 .and_then(|v| v.as_str())
841 .unwrap_or("")
842 .to_string(),
843 display_name: profile
844 .get("display_name")
845 .and_then(|v| v.as_str())
846 .unwrap_or("")
847 .to_string(),
848 is_bot: u.get("is_bot").and_then(|v| v.as_bool()).unwrap_or(false),
849 is_admin: u.get("is_admin").and_then(|v| v.as_bool()).unwrap_or(false),
850 email: profile
851 .get("email")
852 .and_then(|v| v.as_str())
853 .map(|s| s.to_string()),
854 status_text: profile
855 .get("status_text")
856 .and_then(|v| v.as_str())
857 .unwrap_or("")
858 .to_string(),
859 status_emoji: profile
860 .get("status_emoji")
861 .and_then(|v| v.as_str())
862 .unwrap_or("")
863 .to_string(),
864 })
865 }
866
867 async fn reactions_add(
868 &self,
869 channel: &str,
870 timestamp: &str,
871 name: &str,
872 ) -> Result<(), String> {
873 let body = serde_json::json!({ "channel": channel, "timestamp": timestamp, "name": name });
874 self.slack_post("https://slack.com/api/reactions.add", &body)
875 .await?;
876 Ok(())
877 }
878
879 async fn reactions_get(
880 &self,
881 channel: &str,
882 timestamp: &str,
883 ) -> Result<Vec<SlackReaction>, String> {
884 let url = format!(
885 "https://slack.com/api/reactions.get?channel={}×tamp={}&full=true",
886 channel, timestamp
887 );
888 let json = self.slack_get(&url).await?;
889
890 let reactions = json
891 .get("message")
892 .and_then(|m| m.get("reactions"))
893 .and_then(|r| r.as_array())
894 .map(|arr| {
895 arr.iter()
896 .filter_map(|r| {
897 Some(SlackReaction {
898 name: r.get("name")?.as_str()?.to_string(),
899 count: r.get("count").and_then(|c| c.as_u64()).unwrap_or(0),
900 users: r
901 .get("users")
902 .and_then(|u| u.as_array())
903 .map(|a| {
904 a.iter()
905 .filter_map(|v| v.as_str().map(|s| s.to_string()))
906 .collect()
907 })
908 .unwrap_or_default(),
909 })
910 })
911 .collect()
912 })
913 .unwrap_or_default();
914
915 Ok(reactions)
916 }
917
918 async fn files_list(
919 &self,
920 channel: Option<&str>,
921 limit: usize,
922 ) -> Result<Vec<SlackFile>, String> {
923 let mut url = format!("https://slack.com/api/files.list?count={}", limit);
924 if let Some(ch) = channel {
925 url.push_str(&format!("&channel={}", ch));
926 }
927
928 let json = self.slack_get(&url).await?;
929
930 let files = json
931 .get("files")
932 .and_then(|f| f.as_array())
933 .map(|arr| {
934 arr.iter()
935 .filter_map(|f| {
936 Some(SlackFile {
937 id: f.get("id")?.as_str()?.to_string(),
938 name: f
939 .get("name")
940 .and_then(|v| v.as_str())
941 .unwrap_or("unnamed")
942 .to_string(),
943 filetype: f
944 .get("filetype")
945 .and_then(|v| v.as_str())
946 .unwrap_or("")
947 .to_string(),
948 size: f.get("size").and_then(|v| v.as_u64()).unwrap_or(0),
949 url_private: f
950 .get("url_private")
951 .and_then(|v| v.as_str())
952 .unwrap_or("")
953 .to_string(),
954 user: f
955 .get("user")
956 .and_then(|v| v.as_str())
957 .unwrap_or("")
958 .to_string(),
959 timestamp: f.get("timestamp").and_then(|v| v.as_u64()).unwrap_or(0),
960 })
961 })
962 .collect()
963 })
964 .unwrap_or_default();
965
966 Ok(files)
967 }
968
969 async fn team_info(&self) -> Result<SlackTeamInfo, String> {
970 let json = self.slack_get("https://slack.com/api/team.info").await?;
971
972 let team = json.get("team").ok_or("Missing 'team' in response")?;
973 Ok(SlackTeamInfo {
974 id: team
975 .get("id")
976 .and_then(|v| v.as_str())
977 .unwrap_or("")
978 .to_string(),
979 name: team
980 .get("name")
981 .and_then(|v| v.as_str())
982 .unwrap_or("")
983 .to_string(),
984 domain: team
985 .get("domain")
986 .and_then(|v| v.as_str())
987 .unwrap_or("")
988 .to_string(),
989 icon_url: team
990 .get("icon")
991 .and_then(|i| i.get("image_132"))
992 .and_then(|v| v.as_str())
993 .map(|s| s.to_string()),
994 })
995 }
996
997 async fn usergroups_list(&self) -> Result<Vec<SlackUserGroup>, String> {
998 let json = self
999 .slack_get("https://slack.com/api/usergroups.list?include_count=true")
1000 .await?;
1001
1002 let groups = json
1003 .get("usergroups")
1004 .and_then(|g| g.as_array())
1005 .map(|arr| {
1006 arr.iter()
1007 .filter_map(|g| {
1008 Some(SlackUserGroup {
1009 id: g.get("id")?.as_str()?.to_string(),
1010 name: g
1011 .get("name")
1012 .and_then(|v| v.as_str())
1013 .unwrap_or("")
1014 .to_string(),
1015 handle: g
1016 .get("handle")
1017 .and_then(|v| v.as_str())
1018 .unwrap_or("")
1019 .to_string(),
1020 description: g
1021 .get("description")
1022 .and_then(|v| v.as_str())
1023 .unwrap_or("")
1024 .to_string(),
1025 user_count: g.get("user_count").and_then(|v| v.as_u64()).unwrap_or(0),
1026 })
1027 })
1028 .collect()
1029 })
1030 .unwrap_or_default();
1031
1032 Ok(groups)
1033 }
1034
1035 async fn conversations_open(&self, user_ids: &[&str]) -> Result<String, String> {
1036 let body = serde_json::json!({ "users": user_ids.join(",") });
1037 let json = self
1038 .slack_post("https://slack.com/api/conversations.open", &body)
1039 .await?;
1040
1041 json.get("channel")
1042 .and_then(|c| c.get("id"))
1043 .and_then(|id| id.as_str())
1044 .map(|s| s.to_string())
1045 .ok_or_else(|| "Missing 'channel.id' in response".to_string())
1046 }
1047}
1048
1049pub fn create_slack_channel(config: SlackConfig) -> SlackChannel {
1051 let resolved_token = config.resolve_bot_token().unwrap_or_else(|e| {
1052 tracing::warn!(
1053 "Failed to resolve Slack bot token: {}. Falling back to raw value.",
1054 e
1055 );
1056 config.bot_token.as_str().to_string()
1057 });
1058 let http = RealSlackHttp::new(resolved_token);
1059 SlackChannel::new(config, Box::new(http))
1060}
1061
1062#[cfg(test)]
1065mod tests {
1066 use super::*;
1067 use crate::channels::ChannelUser;
1068 use std::sync::{Arc, Mutex};
1069
1070 struct MockSlackHttp {
1071 sent: Arc<Mutex<Vec<(String, String)>>>,
1072 messages: Vec<SlackMessage>,
1073 auth_ok: bool,
1074 }
1075
1076 impl MockSlackHttp {
1077 fn new() -> Self {
1078 Self {
1079 sent: Arc::new(Mutex::new(Vec::new())),
1080 messages: Vec::new(),
1081 auth_ok: true,
1082 }
1083 }
1084
1085 fn with_messages(mut self, messages: Vec<SlackMessage>) -> Self {
1086 self.messages = messages;
1087 self
1088 }
1089 }
1090
1091 #[async_trait]
1092 impl SlackHttpClient for MockSlackHttp {
1093 async fn post_message(&self, channel: &str, text: &str) -> Result<String, String> {
1094 self.sent
1095 .lock()
1096 .unwrap()
1097 .push((channel.to_string(), text.to_string()));
1098 Ok("1234567890.123456".to_string())
1099 }
1100
1101 async fn post_thread_reply(
1102 &self,
1103 channel: &str,
1104 _thread_ts: &str,
1105 text: &str,
1106 ) -> Result<String, String> {
1107 self.sent
1108 .lock()
1109 .unwrap()
1110 .push((channel.to_string(), text.to_string()));
1111 Ok("1234567890.654321".to_string())
1112 }
1113
1114 async fn conversations_history(
1115 &self,
1116 _channel: &str,
1117 _limit: usize,
1118 ) -> Result<Vec<SlackMessage>, String> {
1119 Ok(self.messages.clone())
1120 }
1121
1122 async fn auth_test(&self) -> Result<String, String> {
1123 if self.auth_ok {
1124 Ok("bot-user-id".to_string())
1125 } else {
1126 Err("invalid_auth".to_string())
1127 }
1128 }
1129
1130 async fn conversations_list(
1131 &self,
1132 _types: &str,
1133 _limit: usize,
1134 ) -> Result<Vec<SlackChannelInfo>, String> {
1135 Ok(vec![
1136 SlackChannelInfo {
1137 id: "C001".into(),
1138 name: "general".into(),
1139 is_private: false,
1140 is_member: true,
1141 num_members: 42,
1142 topic: "General chat".into(),
1143 purpose: "Company-wide".into(),
1144 },
1145 SlackChannelInfo {
1146 id: "C002".into(),
1147 name: "random".into(),
1148 is_private: false,
1149 is_member: true,
1150 num_members: 38,
1151 topic: "".into(),
1152 purpose: "Random stuff".into(),
1153 },
1154 ])
1155 }
1156
1157 async fn conversations_join(&self, _channel_id: &str) -> Result<(), String> {
1158 Ok(())
1159 }
1160
1161 async fn conversations_info(&self, channel_id: &str) -> Result<SlackChannelInfo, String> {
1162 Ok(SlackChannelInfo {
1163 id: channel_id.to_string(),
1164 name: "general".into(),
1165 is_private: false,
1166 is_member: true,
1167 num_members: 42,
1168 topic: "General chat".into(),
1169 purpose: "Company-wide".into(),
1170 })
1171 }
1172
1173 async fn users_list(&self, _limit: usize) -> Result<Vec<SlackUserInfo>, String> {
1174 Ok(vec![SlackUserInfo {
1175 id: "U001".into(),
1176 name: "alice".into(),
1177 real_name: "Alice Smith".into(),
1178 display_name: "alice".into(),
1179 is_bot: false,
1180 is_admin: true,
1181 email: Some("alice@example.com".into()),
1182 status_text: "Working".into(),
1183 status_emoji: ":computer:".into(),
1184 }])
1185 }
1186
1187 async fn users_info(&self, user_id: &str) -> Result<SlackUserInfo, String> {
1188 Ok(SlackUserInfo {
1189 id: user_id.to_string(),
1190 name: "alice".into(),
1191 real_name: "Alice Smith".into(),
1192 display_name: "alice".into(),
1193 is_bot: false,
1194 is_admin: true,
1195 email: Some("alice@example.com".into()),
1196 status_text: "Working".into(),
1197 status_emoji: ":computer:".into(),
1198 })
1199 }
1200
1201 async fn reactions_add(
1202 &self,
1203 _channel: &str,
1204 _timestamp: &str,
1205 _name: &str,
1206 ) -> Result<(), String> {
1207 Ok(())
1208 }
1209
1210 async fn reactions_get(
1211 &self,
1212 _channel: &str,
1213 _timestamp: &str,
1214 ) -> Result<Vec<SlackReaction>, String> {
1215 Ok(vec![SlackReaction {
1216 name: "thumbsup".into(),
1217 count: 3,
1218 users: vec!["U001".into(), "U002".into(), "U003".into()],
1219 }])
1220 }
1221
1222 async fn files_list(
1223 &self,
1224 _channel: Option<&str>,
1225 _limit: usize,
1226 ) -> Result<Vec<SlackFile>, String> {
1227 Ok(vec![SlackFile {
1228 id: "F001".into(),
1229 name: "report.pdf".into(),
1230 filetype: "pdf".into(),
1231 size: 1024,
1232 url_private: "https://files.slack.com/report.pdf".into(),
1233 user: "U001".into(),
1234 timestamp: 1700000000,
1235 }])
1236 }
1237
1238 async fn team_info(&self) -> Result<SlackTeamInfo, String> {
1239 Ok(SlackTeamInfo {
1240 id: "T001".into(),
1241 name: "Test Workspace".into(),
1242 domain: "test-workspace".into(),
1243 icon_url: None,
1244 })
1245 }
1246
1247 async fn usergroups_list(&self) -> Result<Vec<SlackUserGroup>, String> {
1248 Ok(vec![SlackUserGroup {
1249 id: "S001".into(),
1250 name: "Engineering".into(),
1251 handle: "engineering".into(),
1252 description: "Engineering team".into(),
1253 user_count: 15,
1254 }])
1255 }
1256
1257 async fn conversations_open(&self, _user_ids: &[&str]) -> Result<String, String> {
1258 Ok("D001".to_string())
1259 }
1260 }
1261
1262 #[tokio::test]
1265 async fn test_slack_connect() {
1266 let config = SlackConfig {
1267 bot_token: "xoxb-123".into(),
1268 ..Default::default()
1269 };
1270 let mut ch = SlackChannel::new(config, Box::new(MockSlackHttp::new()));
1271 ch.connect().await.unwrap();
1272 assert!(ch.is_connected());
1273 }
1274
1275 #[tokio::test]
1276 async fn test_slack_send_message() {
1277 let config = SlackConfig {
1278 bot_token: "xoxb-123".into(),
1279 default_channel: Some("general".into()),
1280 ..Default::default()
1281 };
1282 let http = MockSlackHttp::new();
1283 let sent = http.sent.clone();
1284 let mut ch = SlackChannel::new(config, Box::new(http));
1285 ch.connect().await.unwrap();
1286
1287 let sender = ChannelUser::new("bot", ChannelType::Slack);
1288 let msg = ChannelMessage::text(ChannelType::Slack, "random", sender, "Hello Slack!");
1289 ch.send_message(msg).await.unwrap();
1290
1291 let sent = sent.lock().unwrap();
1292 assert_eq!(sent[0].0, "random");
1293 assert_eq!(sent[0].1, "Hello Slack!");
1294 }
1295
1296 #[tokio::test]
1297 async fn test_slack_receive_messages() {
1298 let config = SlackConfig {
1299 bot_token: "xoxb-123".into(),
1300 allowed_channels: vec!["general".into()],
1301 ..Default::default()
1302 };
1303 let http = MockSlackHttp::new().with_messages(vec![SlackMessage {
1304 ts: "123.456".into(),
1305 channel: "general".into(),
1306 user: "U123".into(),
1307 text: "hey".into(),
1308 thread_ts: None,
1309 }]);
1310 let mut ch = SlackChannel::new(config, Box::new(http));
1311 ch.connect().await.unwrap();
1312
1313 let msgs = ch.receive_messages().await.unwrap();
1314 assert_eq!(msgs.len(), 1);
1315 assert_eq!(msgs[0].content.as_text(), Some("hey"));
1316 }
1317
1318 #[test]
1319 fn test_slack_capabilities() {
1320 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1321 let caps = ch.capabilities();
1322 assert!(caps.supports_threads);
1323 assert!(caps.supports_reactions);
1324 assert!(caps.supports_files);
1325 assert!(!caps.supports_voice);
1326 assert_eq!(caps.max_message_length, Some(40000));
1327 }
1328
1329 #[test]
1330 fn test_slack_streaming_mode() {
1331 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1332 assert_eq!(ch.streaming_mode(), StreamingMode::WebSocket);
1333 }
1334
1335 #[tokio::test]
1336 async fn test_slack_oauth_config_connect() {
1337 let config = SlackConfig {
1338 bot_token: "xoxb-oauth-token-from-oauth-flow".into(),
1339 auth_method: AuthMethod::OAuth,
1340 ..Default::default()
1341 };
1342 let mut ch = SlackChannel::new(config, Box::new(MockSlackHttp::new()));
1343 ch.connect().await.unwrap();
1344 assert!(ch.is_connected());
1345 }
1346
1347 #[test]
1348 fn test_slack_config_auth_method_default() {
1349 let config = SlackConfig::default();
1350 assert_eq!(config.auth_method, AuthMethod::ApiKey);
1351 }
1352
1353 #[test]
1354 fn test_slack_config_auth_method_serde() {
1355 let config = SlackConfig {
1356 bot_token: "xoxb-test".into(),
1357 auth_method: AuthMethod::OAuth,
1358 ..Default::default()
1359 };
1360 let json = serde_json::to_string(&config).unwrap();
1361 assert!(json.contains("\"oauth\""));
1362 let parsed: SlackConfig = serde_json::from_str(&json).unwrap();
1363 assert_eq!(parsed.auth_method, AuthMethod::OAuth);
1364 }
1365
1366 #[tokio::test]
1369 async fn test_slack_list_channels() {
1370 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1371 let channels = ch.list_channels().await.unwrap();
1372 assert_eq!(channels.len(), 2);
1373 assert_eq!(channels[0].name, "general");
1374 assert_eq!(channels[1].name, "random");
1375 assert_eq!(channels[0].num_members, 42);
1376 }
1377
1378 #[tokio::test]
1379 async fn test_slack_channel_info() {
1380 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1381 let info = ch.channel_info("C001").await.unwrap();
1382 assert_eq!(info.id, "C001");
1383 assert_eq!(info.name, "general");
1384 }
1385
1386 #[tokio::test]
1387 async fn test_slack_join_channel() {
1388 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1389 ch.join_channel("C002").await.unwrap();
1390 }
1391
1392 #[tokio::test]
1393 async fn test_slack_list_users() {
1394 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1395 let users = ch.list_users().await.unwrap();
1396 assert_eq!(users.len(), 1);
1397 assert_eq!(users[0].name, "alice");
1398 assert_eq!(users[0].real_name, "Alice Smith");
1399 assert!(users[0].is_admin);
1400 assert!(!users[0].is_bot);
1401 }
1402
1403 #[tokio::test]
1404 async fn test_slack_get_user_info() {
1405 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1406 let user = ch.get_user_info("U001").await.unwrap();
1407 assert_eq!(user.id, "U001");
1408 assert_eq!(user.email, Some("alice@example.com".into()));
1409 }
1410
1411 #[tokio::test]
1412 async fn test_slack_add_reaction() {
1413 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1414 ch.add_reaction("C001", "123.456", "thumbsup")
1415 .await
1416 .unwrap();
1417 }
1418
1419 #[tokio::test]
1420 async fn test_slack_get_reactions() {
1421 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1422 let reactions = ch.get_reactions("C001", "123.456").await.unwrap();
1423 assert_eq!(reactions.len(), 1);
1424 assert_eq!(reactions[0].name, "thumbsup");
1425 assert_eq!(reactions[0].count, 3);
1426 assert_eq!(reactions[0].users.len(), 3);
1427 }
1428
1429 #[tokio::test]
1430 async fn test_slack_list_files() {
1431 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1432 let files = ch.list_files(None).await.unwrap();
1433 assert_eq!(files.len(), 1);
1434 assert_eq!(files[0].name, "report.pdf");
1435 assert_eq!(files[0].size, 1024);
1436 }
1437
1438 #[tokio::test]
1439 async fn test_slack_team_info() {
1440 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1441 let team = ch.get_team_info().await.unwrap();
1442 assert_eq!(team.name, "Test Workspace");
1443 assert_eq!(team.domain, "test-workspace");
1444 }
1445
1446 #[tokio::test]
1447 async fn test_slack_list_usergroups() {
1448 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1449 let groups = ch.list_usergroups().await.unwrap();
1450 assert_eq!(groups.len(), 1);
1451 assert_eq!(groups[0].handle, "engineering");
1452 assert_eq!(groups[0].user_count, 15);
1453 }
1454
1455 #[tokio::test]
1456 async fn test_slack_open_dm() {
1457 let ch = SlackChannel::new(SlackConfig::default(), Box::new(MockSlackHttp::new()));
1458 let dm_channel = ch.open_dm(&["U001"]).await.unwrap();
1459 assert_eq!(dm_channel, "D001");
1460 }
1461
1462 #[tokio::test]
1463 async fn test_slack_thread_reply() {
1464 let http = MockSlackHttp::new();
1465 let sent = http.sent.clone();
1466 let ch = SlackChannel::new(SlackConfig::default(), Box::new(http));
1467 let id = ch
1468 .reply_in_thread("C001", "123.456", "reply text")
1469 .await
1470 .unwrap();
1471 assert!(!id.0.is_empty());
1472 let sent = sent.lock().unwrap();
1473 assert_eq!(sent[0].1, "reply text");
1474 }
1475
1476 #[tokio::test]
1477 async fn test_slack_read_history() {
1478 let http = MockSlackHttp::new().with_messages(vec![SlackMessage {
1479 ts: "999.888".into(),
1480 channel: "test".into(),
1481 user: "U001".into(),
1482 text: "history msg".into(),
1483 thread_ts: None,
1484 }]);
1485 let ch = SlackChannel::new(SlackConfig::default(), Box::new(http));
1486 let msgs = ch.read_history("test", 10).await.unwrap();
1487 assert_eq!(msgs.len(), 1);
1488 assert_eq!(msgs[0].text, "history msg");
1489 }
1490}