Skip to content

Commit ef58544

Browse files
dbieberclaude
andcommitted
Add Slack uploader for note integration
- Add new Slack uploader implementation - Integrate into settings UI - Add required dependencies - Support for posting notes to Slack channels - Create threaded notes with indentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a4516d5 commit ef58544

File tree

6 files changed

+179
-0
lines changed

6 files changed

+179
-0
lines changed

gonotego/settings-server/src/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const SettingsUI = () => {
1818
{ display: 'IdeaFlow', value: 'ideaflow' },
1919
{ display: 'Mem', value: 'mem' },
2020
{ display: 'Notion', value: 'notion' },
21+
{ display: 'Slack', value: 'slack' },
2122
{ display: 'Twitter', value: 'twitter' },
2223
{ display: 'Email', value: 'email' }
2324
];
@@ -41,6 +42,8 @@ const SettingsUI = () => {
4142
MEM_API_KEY: '',
4243
NOTION_INTEGRATION_TOKEN: '',
4344
NOTION_DATABASE_ID: '',
45+
SLACK_API_TOKEN: '',
46+
SLACK_CHANNEL: '',
4447
TWITTER_API_KEY: '',
4548
TWITTER_API_SECRET: '',
4649
TWITTER_ACCESS_TOKEN: '',
@@ -721,6 +724,11 @@ const SettingsUI = () => {
721724
{ key: 'NOTION_DATABASE_ID', label: 'Database ID' },
722725
], shouldShowSection('notion'))}
723726

727+
{renderSettingGroup('Slack', 'Slack integration settings', [
728+
{ key: 'SLACK_API_TOKEN', label: 'API Token', type: 'password', tip: 'Bot token starting with xoxb-' },
729+
{ key: 'SLACK_CHANNEL', label: 'Channel Name', tip: 'Channel name without the # symbol' },
730+
], shouldShowSection('slack'))}
731+
724732
{renderSettingGroup('Twitter API', 'Twitter API credentials', [
725733
{ key: 'TWITTER_API_KEY', label: 'API Key', type: 'password' },
726734
{ key: 'TWITTER_API_SECRET', label: 'API Secret', type: 'password' },

gonotego/settings/secure_settings_template.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
TWITTER_ACCESS_TOKEN = '<TWITTER_ACCESS_TOKEN>'
2424
TWITTER_ACCESS_TOKEN_SECRET = '<TWITTER_ACCESS_TOKEN_SECRET>'
2525

26+
SLACK_API_TOKEN = '<SLACK_API_TOKEN>'
27+
SLACK_CHANNEL = '<SLACK_CHANNEL>'
28+
2629
EMAIL = '<EMAIL>'
2730
EMAIL_USER = '<EMAIL_USER>'
2831
EMAIL_PASSWORD = '<EMAIL_PASSWORD>'

gonotego/uploader/runner.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from gonotego.uploader.roam import roam_uploader
1313
from gonotego.uploader.mem import mem_uploader
1414
from gonotego.uploader.notion import notion_uploader
15+
from gonotego.uploader.slack import slack_uploader
1516
from gonotego.uploader.twitter import twitter_uploader
1617

1718
Status = status.Status
@@ -41,6 +42,8 @@ def make_uploader(note_taking_system):
4142
return mem_uploader.Uploader()
4243
elif note_taking_system == 'notion':
4344
return notion_uploader.Uploader()
45+
elif note_taking_system == 'slack':
46+
return slack_uploader.Uploader()
4447
elif note_taking_system == 'twitter':
4548
return twitter_uploader.Uploader()
4649
else:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Slack uploader package."""
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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\nThis 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()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ dependencies = [
3535
# selenium 4.0 breaks with arm geckodriver.
3636
'selenium==3.141.0',
3737
'setuptools-rust<=1.5.2',
38+
'slack-sdk<=3.26.0', # For Slack integration
3839
'sounddevice<=0.4.5',
3940
'soundfile',
4041
'supervisor<=4.2.4',

0 commit comments

Comments
 (0)