1+ """Uploader for Slack workspace channels."""
2+
3+ import os
4+ import time
5+ import logging
6+ from typing import List , Optional
7+
8+ from slack_sdk import WebClient
9+ from slack_sdk .errors import SlackApiError
10+
11+ from gonotego .common import events
12+ from gonotego .settings import settings
13+
14+ logger = logging .getLogger (__name__ )
15+
16+ class Uploader :
17+ """Uploader implementation for Slack."""
18+
19+ def __init__ (self ):
20+ self ._client : Optional [WebClient ] = None
21+ self ._channel_id : Optional [str ] = None
22+ self ._thread_ts : Optional [str ] = None
23+ self ._session_started : bool = False
24+
25+ @property
26+ def client (self ) -> WebClient :
27+ """Get or create the Slack WebClient."""
28+ if self ._client :
29+ return self ._client
30+
31+ # Get token from settings
32+ token = settings .get ('SLACK_API_TOKEN' )
33+ if not token :
34+ logger .error ("Missing Slack API token in settings" )
35+ raise ValueError ("Missing Slack API token in settings" )
36+
37+ # Initialize the client
38+ self ._client = WebClient (token = token )
39+ return self ._client
40+
41+ def _get_channel_id (self ) -> str :
42+ """Get the channel ID for the configured channel name."""
43+ if self ._channel_id :
44+ return self ._channel_id
45+
46+ channel_name = settings .get ('SLACK_CHANNEL' )
47+ if not channel_name :
48+ logger .error ("Missing Slack channel name in settings" )
49+ raise ValueError ("Missing Slack channel name in settings" )
50+
51+ # Try to find the channel in the workspace
52+ try :
53+ result = self .client .conversations_list ()
54+ for channel in result ['channels' ]:
55+ if channel ['name' ] == channel_name :
56+ self ._channel_id = channel ['id' ]
57+ return self ._channel_id
58+ except SlackApiError as e :
59+ logger .error (f"Error fetching channels: { e } " )
60+ raise
61+
62+ logger .error (f"Channel { channel_name } not found in workspace" )
63+ raise ValueError (f"Channel { channel_name } not found in workspace" )
64+
65+ def _start_session (self , first_note : str ) -> bool :
66+ """Start a new session thread in the configured Slack channel."""
67+ channel_id = self ._get_channel_id ()
68+
69+ # Create the initial message with the note content
70+ try :
71+ message_text = f"{ first_note } \n \n This is a Go Note Go generated thread. :keyboard:"
72+ response = self .client .chat_postMessage (
73+ channel = channel_id ,
74+ text = message_text
75+ )
76+ self ._thread_ts = response ['ts' ]
77+ self ._session_started = True
78+ return True
79+ except SlackApiError as e :
80+ logger .error (f"Error starting session: { e } " )
81+ return False
82+
83+ def _send_note_to_thread (self , text : str , indent_level : int = 0 ) -> bool :
84+ """Send a note as a reply in the current thread."""
85+ if not self ._thread_ts :
86+ logger .error ("Trying to send to thread but no thread exists" )
87+ return False
88+
89+ channel_id = self ._get_channel_id ()
90+
91+ # Format the text based on indentation
92+ formatted_text = text
93+ if indent_level > 0 :
94+ # Add bullet and proper indentation
95+ bullet = "•"
96+ indentation = " " * (indent_level - 1 )
97+ formatted_text = f"{ indentation } { bullet } { text } "
98+
99+ try :
100+ self .client .chat_postMessage (
101+ channel = channel_id ,
102+ text = formatted_text ,
103+ thread_ts = self ._thread_ts
104+ )
105+ return True
106+ except SlackApiError as e :
107+ logger .error (f"Error sending note to thread: { e } " )
108+ return False
109+
110+ def upload (self , note_events : List [events .NoteEvent ]) -> bool :
111+ """Upload note events to Slack.
112+
113+ Args:
114+ note_events: List of NoteEvent objects.
115+
116+ Returns:
117+ bool: True if upload successful, False otherwise.
118+ """
119+ if not note_events :
120+ return True
121+
122+ try :
123+ for note_event in note_events :
124+ if note_event .action == events .SUBMIT :
125+ text = note_event .text .strip ()
126+
127+ # Skip empty notes
128+ if not text :
129+ continue
130+
131+ # Start a new session for the first note
132+ if not self ._session_started :
133+ success = self ._start_session (text )
134+ else :
135+ # Send as a reply to the thread with proper indentation
136+ success = self ._send_note_to_thread (text , note_event .indent_level )
137+
138+ if not success :
139+ logger .error ("Failed to upload note to Slack" )
140+ return False
141+
142+ elif note_event .action == events .END_SESSION :
143+ self .end_session ()
144+
145+ return True
146+ except Exception as e :
147+ logger .exception (f"Error uploading notes to Slack: { e } " )
148+ return False
149+
150+ def end_session (self ) -> None :
151+ """End the current session."""
152+ self ._thread_ts = None
153+ self ._session_started = False
154+
155+ def handle_inactivity (self ) -> None :
156+ """Handle inactivity by ending the session and clearing client."""
157+ self ._client = None
158+ self .end_session ()
159+
160+ def handle_disconnect (self ) -> None :
161+ """Handle disconnection by ending the session and clearing client."""
162+ self ._client = None
163+ self .end_session ()
0 commit comments