Skip to content

Commit 01981f5

Browse files
JCMarques15dokterbobasvishnyakovhayescode
authored
[Add] Implement Slack Socket Mode support (#1436)
## Description This PR introduces support for Slack Socket Mode, enabling real-time message processing using websockets. This feature enhances the Slack integration by allowing for more efficient and responsive communication, and a way to bypass ingress restrictive networks. ## Changes - Added Slack Socket Mode handler in `server.py` - Created `start_socket_mode` function in `slack/app.py` - Implemented conditional logic to use Socket Mode when `SLACK_WEBSOCKET_TOKEN` is set - Added an example script `slack_websocket_test.py` for testing the Slack websocket handler ## How to Test 1. Set the following environment variables: - `SLACK_BOT_TOKEN` - `SLACK_SIGNING_SECRET` - `SLACK_WEBSOCKET_TOKEN` 2. Run the application and verify that the Slack integration connects using Socket Mode 3. Test sending messages through Slack and ensure they are processed correctly ## Additional Notes - The existing HTTP handler remains active when only `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET` are provided - When `SLACK_WEBSOCKET_TOKEN` is set, the application will prioritize Socket Mode over the HTTP handler --------- Co-authored-by: Mathijs de Bruin <[email protected]> Co-authored-by: Aleksandr Vishniakov <[email protected]> Co-authored-by: Josh Hayes <[email protected]>
1 parent 78cb1ac commit 01981f5

File tree

4 files changed

+85
-3
lines changed

4 files changed

+85
-3
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pnpm-debug.log*
4040
lerna-debug.log*
4141

4242
node_modules
43+
.pnpm-store
4344
dist
4445
dist-ssr
4546
*.local
@@ -61,4 +62,4 @@ dist-ssr
6162
backend/README.md
6263
backend/.dmypy.json
6364

64-
.history
65+
.history

backend/chainlit/server.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ async def watch_files_for_changes():
147147

148148
discord_task = asyncio.create_task(client.start(discord_bot_token))
149149

150+
slack_task = None
151+
152+
# Slack Socket Handler if env variable SLACK_WEBSOCKET_TOKEN is set
153+
if os.environ.get("SLACK_BOT_TOKEN") and os.environ.get("SLACK_WEBSOCKET_TOKEN"):
154+
from chainlit.slack.app import start_socket_mode
155+
156+
slack_task = asyncio.create_task(start_socket_mode())
157+
150158
try:
151159
yield
152160
finally:
@@ -162,6 +170,10 @@ async def watch_files_for_changes():
162170
if discord_task:
163171
discord_task.cancel()
164172
await discord_task
173+
174+
if slack_task:
175+
slack_task.cancel()
176+
await slack_task
165177
except asyncio.exceptions.CancelledError:
166178
pass
167179

@@ -294,10 +306,14 @@ async def serve_copilot_file(
294306

295307

296308
# -------------------------------------------------------------------------------
297-
# SLACK HANDLER
309+
# SLACK HTTP HANDLER
298310
# -------------------------------------------------------------------------------
299311

300-
if os.environ.get("SLACK_BOT_TOKEN") and os.environ.get("SLACK_SIGNING_SECRET"):
312+
if (
313+
os.environ.get("SLACK_BOT_TOKEN")
314+
and os.environ.get("SLACK_SIGNING_SECRET")
315+
and not os.environ.get("SLACK_WEBSOCKET_TOKEN")
316+
):
301317
from chainlit.slack.app import slack_app_handler
302318

303319
@router.post("/slack/events")

backend/chainlit/slack/app.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import httpx
99
from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler
10+
from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
1011
from slack_bolt.async_app import AsyncApp
1112

1213
from chainlit.config import config
@@ -125,6 +126,16 @@ async def update_step(self, step_dict: StepDict):
125126
)
126127

127128

129+
async def start_socket_mode():
130+
"""
131+
Initializes and starts the Slack app in Socket Mode asynchronously.
132+
133+
Uses the SLACK_WEBSOCKET_TOKEN from environment variables to authenticate.
134+
"""
135+
handler = AsyncSocketModeHandler(slack_app, os.environ.get("SLACK_WEBSOCKET_TOKEN"))
136+
await handler.start_async()
137+
138+
128139
def init_slack_context(
129140
session: HTTPSession,
130141
slack_channel_id: str,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# tests/test_slack_socket_mode.py
2+
import importlib
3+
from unittest.mock import AsyncMock, patch
4+
5+
import pytest
6+
7+
8+
@pytest.mark.asyncio
9+
async def test_start_socket_mode_starts_handler(monkeypatch):
10+
"""
11+
The function should:
12+
• build an AsyncSocketModeHandler with the global slack_app
13+
• use the token found in SLACK_WEBSOCKET_TOKEN
14+
• await the handler.start_async() coroutine exactly once
15+
"""
16+
token = "xapp-fake-token"
17+
# minimal env required for the Slack module to initialise
18+
monkeypatch.setenv("SLACK_BOT_TOKEN", "xoxb-fake-bot")
19+
monkeypatch.setenv("SLACK_WEBSOCKET_TOKEN", token)
20+
21+
# Import the module first to avoid lazy import registry issues
22+
slack_app_mod = importlib.import_module("chainlit.slack.app")
23+
24+
# Patch the object directly instead of using string path
25+
with patch.object(
26+
slack_app_mod, "AsyncSocketModeHandler", autospec=True
27+
) as handler_cls:
28+
handler_instance = AsyncMock()
29+
handler_cls.return_value = handler_instance
30+
31+
# Run: should build handler + await start_async
32+
await slack_app_mod.start_socket_mode()
33+
34+
handler_cls.assert_called_once_with(slack_app_mod.slack_app, token)
35+
handler_instance.start_async.assert_awaited_once()
36+
37+
38+
def test_slack_http_route_registered(monkeypatch):
39+
"""
40+
When only the classic HTTP tokens are set (no websocket token),
41+
the FastAPI app should expose POST /slack/events.
42+
"""
43+
# HTTP-only environment
44+
monkeypatch.setenv("SLACK_BOT_TOKEN", "xoxb-fake-bot")
45+
monkeypatch.setenv("SLACK_SIGNING_SECRET", "shhh-fake-secret")
46+
monkeypatch.delenv("SLACK_WEBSOCKET_TOKEN", raising=False)
47+
48+
# Re-import server with the fresh env so the route table is built correctly
49+
server = importlib.reload(importlib.import_module("chainlit.server"))
50+
51+
assert any(
52+
route.path == "/slack/events" and "POST" in route.methods
53+
for route in server.router.routes
54+
), "Slack HTTP handler route was not registered"

0 commit comments

Comments
 (0)