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(¶ms)
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}