Skip to main content

synaptic_slack/
lib.rs

1use async_trait::async_trait;
2use serde_json::Value;
3use std::collections::HashMap;
4use synaptic_core::{Document, Loader, SynapticError};
5
6#[derive(Debug, Clone)]
7pub struct SlackConfig {
8    pub bot_token: String,
9    pub channel_ids: Vec<String>,
10    pub oldest: Option<String>,
11    pub limit: usize,
12    pub include_threads: bool,
13}
14
15impl SlackConfig {
16    pub fn new(bot_token: impl Into<String>, channel_ids: Vec<String>) -> Self {
17        Self {
18            bot_token: bot_token.into(),
19            channel_ids,
20            oldest: None,
21            limit: 100,
22            include_threads: false,
23        }
24    }
25
26    pub fn with_oldest(mut self, ts: impl Into<String>) -> Self {
27        self.oldest = Some(ts.into());
28        self
29    }
30
31    pub fn with_limit(mut self, limit: usize) -> Self {
32        self.limit = limit;
33        self
34    }
35
36    pub fn with_threads(mut self) -> Self {
37        self.include_threads = true;
38        self
39    }
40}
41
42pub struct SlackLoader {
43    config: SlackConfig,
44    client: reqwest::Client,
45}
46
47impl SlackLoader {
48    pub fn new(config: SlackConfig) -> Self {
49        Self {
50            config,
51            client: reqwest::Client::new(),
52        }
53    }
54
55    async fn fetch_messages(&self, channel_id: &str) -> Result<Vec<Value>, SynapticError> {
56        let mut params = vec![
57            ("channel", channel_id.to_string()),
58            ("limit", self.config.limit.to_string()),
59        ];
60        if let Some(ref oldest) = self.config.oldest {
61            params.push(("oldest", oldest.clone()));
62        }
63
64        let resp = self
65            .client
66            .get("https://slack.com/api/conversations.history")
67            .bearer_auth(&self.config.bot_token)
68            .query(&params)
69            .send()
70            .await
71            .map_err(|e| SynapticError::Loader(format!("Slack fetch: {e}")))?;
72
73        let body: Value = resp
74            .json()
75            .await
76            .map_err(|e| SynapticError::Loader(format!("Slack parse: {e}")))?;
77
78        if !body["ok"].as_bool().unwrap_or(false) {
79            return Err(SynapticError::Loader(format!(
80                "Slack API error: {}",
81                body["error"].as_str().unwrap_or("unknown")
82            )));
83        }
84
85        Ok(body["messages"].as_array().cloned().unwrap_or_default())
86    }
87}
88
89#[async_trait]
90impl Loader for SlackLoader {
91    async fn load(&self) -> Result<Vec<Document>, SynapticError> {
92        let mut documents = Vec::new();
93        for channel_id in &self.config.channel_ids {
94            let messages = self.fetch_messages(channel_id).await?;
95            for msg in messages {
96                let text = msg["text"].as_str().unwrap_or("").to_string();
97                if text.is_empty() {
98                    continue;
99                }
100                let ts = msg["ts"].as_str().unwrap_or("").to_string();
101                let user = msg["user"].as_str().unwrap_or("").to_string();
102                let doc_id = format!("{}-{}", channel_id, ts);
103
104                let mut metadata = HashMap::new();
105                metadata.insert("channel".to_string(), Value::String(channel_id.clone()));
106                metadata.insert("ts".to_string(), Value::String(ts));
107                metadata.insert("user".to_string(), Value::String(user));
108                metadata.insert(
109                    "source".to_string(),
110                    Value::String(format!("slack:{}", channel_id)),
111                );
112                if let Some(thread_ts) = msg["thread_ts"].as_str() {
113                    metadata.insert(
114                        "thread_ts".to_string(),
115                        Value::String(thread_ts.to_string()),
116                    );
117                }
118
119                documents.push(Document {
120                    id: doc_id,
121                    content: text,
122                    metadata,
123                });
124            }
125        }
126        Ok(documents)
127    }
128}