11"""Slack connector — sync channel history to a Knowledge Base.
22
33Auth via SLACK_TOKEN env var (Bot User OAuth Token with channels:history scope).
4+ Messages are split by day for incremental sync — past days never change.
45"""
56
67from __future__ import annotations
78
89import hashlib
910import os
11+ from collections import defaultdict
12+ from datetime import datetime , timezone
1013from typing import Any
1114
1215import httpx
1518
1619
1720class SlackConnector (BaseConnector ):
18- """Sync messages from a Slack channel."""
21+ """Sync messages from a Slack channel, one file per day ."""
1922
20- def __init__ (self , channel_id : str , token : str | None = None , limit : int = 1000 ):
23+ def __init__ (self , channel_id : str , token : str | None = None , limit : int = 5000 ):
2124 self .channel_id = channel_id
2225 self .limit = limit
2326 self ._token = token or os .environ .get ("SLACK_TOKEN" )
@@ -29,27 +32,61 @@ def __init__(self, channel_id: str, token: str | None = None, limit: int = 1000)
2932 headers = {"Authorization" : f"Bearer { self ._token } " },
3033 timeout = 30.0 ,
3134 )
32- self ._messages : list [dict ] = []
35+ self ._channel_name : str = ""
36+ self ._daily_texts : dict [str , str ] = {}
3337
3438 def build_manifest (self ) -> list [ManifestEntry ]:
35- self . _messages = self ._fetch_history ()
36- if not self . _messages :
39+ messages = self ._fetch_history ()
40+ if not messages :
3741 return []
3842
39- text = self ._format_messages (self ._messages )
40- checksum = hashlib .sha256 (text .encode ()).hexdigest ()[:16 ]
41-
4243 # Get channel name.
4344 info = self ._http .get ("/conversations.info" , params = {"channel" : self .channel_id })
44- name = info .json ().get ("channel" , {}).get ("name" , self .channel_id ) if info .status_code == 200 else self .channel_id
45+ self ._channel_name = (
46+ info .json ().get ("channel" , {}).get ("name" , self .channel_id )
47+ if info .status_code == 200
48+ else self .channel_id
49+ )
4550
46- return [ManifestEntry (filename = f"{ name } .txt" , path = "" , checksum = checksum , size = len (text .encode ()))]
51+ # Group messages by date.
52+ by_day : dict [str , list [dict ]] = defaultdict (list )
53+ for msg in messages :
54+ ts = float (msg .get ("ts" , "0" ))
55+ day = datetime .fromtimestamp (ts , tz = timezone .utc ).strftime ("%Y-%m-%d" )
56+ by_day [day ].append (msg )
57+
58+ entries : list [ManifestEntry ] = []
59+ for day , day_msgs in sorted (by_day .items ()):
60+ lines = []
61+ for msg in sorted (day_msgs , key = lambda m : m .get ("ts" , "" )):
62+ user = msg .get ("user" , "unknown" )
63+ text = msg .get ("text" , "" )
64+ ts = msg .get ("ts" , "" )
65+ lines .append (f"[{ ts } ] { user } : { text } " )
66+
67+ content = "\n " .join (lines )
68+ self ._daily_texts [day ] = content
69+ checksum = hashlib .sha256 (content .encode ()).hexdigest ()[:16 ]
70+
71+ entries .append (
72+ ManifestEntry (
73+ filename = f"{ self ._channel_name } _{ day } .txt" ,
74+ path = "" ,
75+ checksum = checksum ,
76+ size = len (content .encode ()),
77+ )
78+ )
79+
80+ return entries
4781
4882 def _fetch_history (self ) -> list [dict ]:
4983 messages : list [dict ] = []
5084 cursor = None
5185 while len (messages ) < self .limit :
52- params : dict [str , Any ] = {"channel" : self .channel_id , "limit" : min (200 , self .limit - len (messages ))}
86+ params : dict [str , Any ] = {
87+ "channel" : self .channel_id ,
88+ "limit" : min (200 , self .limit - len (messages )),
89+ }
5390 if cursor :
5491 params ["cursor" ] = cursor
5592 resp = self ._http .get ("/conversations.history" , params = params )
@@ -61,17 +98,12 @@ def _fetch_history(self) -> list[dict]:
6198 break
6299 return messages
63100
64- def _format_messages (self , messages : list [dict ]) -> str :
65- lines = []
66- for msg in reversed (messages ):
67- user = msg .get ("user" , "unknown" )
68- text = msg .get ("text" , "" )
69- ts = msg .get ("ts" , "" )
70- lines .append (f"[{ ts } ] { user } : { text } " )
71- return "\n " .join (lines )
72-
73101 def read_file (self , path : str , filename : str ) -> bytes :
74- return self ._format_messages (self ._messages ).encode ("utf-8" )
102+ # Extract date from filename: channel_YYYY-MM-DD.txt
103+ for day , text in self ._daily_texts .items ():
104+ if day in filename :
105+ return text .encode ("utf-8" )
106+ raise FileNotFoundError (f"Day not found: { filename } " )
75107
76108 def close (self ) -> None :
77109 self ._http .close ()
0 commit comments