rustyclaw_core/messengers/
slack.rs1use super::{Message, Messenger, SendOptions};
7use anyhow::{Context, Result};
8use async_trait::async_trait;
9use std::sync::Mutex;
10
11pub struct SlackMessenger {
13 name: String,
14 bot_token: String,
15 app_token: Option<String>,
17 default_channel: Option<String>,
19 connected: bool,
20 http: reqwest::Client,
21 bot_user_id: Option<String>,
23 last_ts: Mutex<std::collections::HashMap<String, String>>,
26}
27
28impl SlackMessenger {
29 pub fn new(name: String, bot_token: String) -> Self {
30 Self {
31 name,
32 bot_token,
33 app_token: None,
34 default_channel: None,
35 connected: false,
36 http: reqwest::Client::new(),
37 bot_user_id: None,
38 last_ts: Mutex::new(std::collections::HashMap::new()),
39 }
40 }
41
42 pub fn with_app_token(mut self, token: String) -> Self {
44 self.app_token = Some(token);
45 self
46 }
47
48 pub fn with_default_channel(mut self, channel: String) -> Self {
50 self.default_channel = Some(channel);
51 self
52 }
53
54 fn api_url(method: &str) -> String {
55 format!("https://slack.com/api/{}", method)
56 }
57
58 async fn api_call(&self, method: &str, body: &serde_json::Value) -> Result<serde_json::Value> {
60 let resp = self
61 .http
62 .post(Self::api_url(method))
63 .header("Authorization", format!("Bearer {}", self.bot_token))
64 .header("Content-Type", "application/json; charset=utf-8")
65 .json(body)
66 .send()
67 .await
68 .with_context(|| format!("Slack API call to {} failed", method))?;
69
70 let status = resp.status();
71 let data: serde_json::Value = resp
72 .json()
73 .await
74 .with_context(|| format!("Failed to parse Slack {} response", method))?;
75
76 if !status.is_success() {
77 anyhow::bail!("Slack {} returned HTTP {}", method, status);
78 }
79
80 if data["ok"].as_bool() != Some(true) {
81 let error = data["error"].as_str().unwrap_or("unknown_error");
82 anyhow::bail!("Slack {} error: {}", method, error);
83 }
84
85 Ok(data)
86 }
87
88 async fn fetch_history(&self, channel: &str) -> Result<Vec<Message>> {
90 let mut params = serde_json::json!({
91 "channel": channel,
92 "limit": 20
93 });
94
95 {
96 let last_ts = self.last_ts.lock().unwrap();
97 if let Some(ts) = last_ts.get(channel) {
98 params["oldest"] = serde_json::json!(ts);
99 }
100 }
101
102 let data = self.api_call("conversations.history", ¶ms).await?;
103
104 let messages_arr = data["messages"]
105 .as_array()
106 .cloned()
107 .unwrap_or_default();
108
109 let mut result = Vec::new();
110
111 for msg in &messages_arr {
112 if let Some(user) = msg["user"].as_str() {
114 if Some(user) == self.bot_user_id.as_deref() {
115 continue;
116 }
117 }
118
119 let text = match msg["text"].as_str() {
121 Some(t) if !t.is_empty() => t,
122 _ => continue,
123 };
124
125 let ts = msg["ts"].as_str().unwrap_or("0").to_string();
126 let sender = msg["user"].as_str().unwrap_or("unknown").to_string();
127
128 result.push(Message {
129 id: ts.clone(),
130 sender,
131 content: text.to_string(),
132 timestamp: parse_slack_ts(&ts),
133 channel: Some(channel.to_string()),
134 reply_to: msg["thread_ts"].as_str().map(|s| s.to_string()),
135 media: None,
136 });
137 }
138
139 if let Some(newest) = messages_arr
141 .first()
142 .and_then(|m| m["ts"].as_str())
143 {
144 let mut last_ts = self.last_ts.lock().unwrap();
145 last_ts.insert(channel.to_string(), newest.to_string());
146 }
147
148 result.reverse();
150 Ok(result)
151 }
152}
153
154fn parse_slack_ts(ts: &str) -> i64 {
156 ts.split('.')
157 .next()
158 .and_then(|s| s.parse::<i64>().ok())
159 .unwrap_or(0)
160}
161
162#[async_trait]
163impl Messenger for SlackMessenger {
164 fn name(&self) -> &str {
165 &self.name
166 }
167
168 fn messenger_type(&self) -> &str {
169 "slack"
170 }
171
172 async fn initialize(&mut self) -> Result<()> {
173 let data = self
175 .api_call("auth.test", &serde_json::json!({}))
176 .await
177 .context("Slack auth.test failed — check your bot token")?;
178
179 self.bot_user_id = data["user_id"].as_str().map(|s| s.to_string());
180 self.connected = true;
181
182 tracing::info!(
183 bot_user = ?self.bot_user_id,
184 team = ?data["team"].as_str(),
185 "Slack connected"
186 );
187
188 Ok(())
189 }
190
191 async fn send_message(&self, channel: &str, content: &str) -> Result<String> {
192 let data = self
193 .api_call(
194 "chat.postMessage",
195 &serde_json::json!({
196 "channel": channel,
197 "text": content,
198 "unfurl_links": false,
199 }),
200 )
201 .await?;
202
203 Ok(data["ts"].as_str().unwrap_or("unknown").to_string())
204 }
205
206 async fn send_message_with_options(&self, opts: SendOptions<'_>) -> Result<String> {
207 let mut payload = serde_json::json!({
208 "channel": opts.recipient,
209 "text": opts.content,
210 "unfurl_links": false,
211 });
212
213 if let Some(thread_ts) = opts.reply_to {
215 payload["thread_ts"] = serde_json::json!(thread_ts);
216 }
217
218 let data = self.api_call("chat.postMessage", &payload).await?;
219 Ok(data["ts"].as_str().unwrap_or("unknown").to_string())
220 }
221
222 async fn receive_messages(&self) -> Result<Vec<Message>> {
223 let channel = match &self.default_channel {
224 Some(ch) => ch.clone(),
225 None => return Ok(Vec::new()),
226 };
227 self.fetch_history(&channel).await
228 }
229
230 fn is_connected(&self) -> bool {
231 self.connected
232 }
233
234 async fn disconnect(&mut self) -> Result<()> {
235 self.connected = false;
236 Ok(())
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_slack_messenger_creation() {
246 let messenger = SlackMessenger::new("test".to_string(), "xoxb-fake".to_string());
247 assert_eq!(messenger.name(), "test");
248 assert_eq!(messenger.messenger_type(), "slack");
249 assert!(!messenger.is_connected());
250 }
251
252 #[test]
253 fn test_parse_slack_ts() {
254 assert_eq!(parse_slack_ts("1234567890.123456"), 1234567890);
255 assert_eq!(parse_slack_ts("0"), 0);
256 assert_eq!(parse_slack_ts(""), 0);
257 }
258
259 #[test]
260 fn test_with_options() {
261 let messenger = SlackMessenger::new("test".to_string(), "xoxb-fake".to_string())
262 .with_app_token("xapp-fake".to_string())
263 .with_default_channel("C12345".to_string());
264 assert_eq!(messenger.app_token, Some("xapp-fake".to_string()));
265 assert_eq!(messenger.default_channel, Some("C12345".to_string()));
266 }
267}