-
Notifications
You must be signed in to change notification settings - Fork 7
Slack Bot template app #315
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
nealrichardson
wants to merge
3
commits into
main
Choose a base branch
from
slack-bot
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # Slack Bot Token (xoxb-...) | ||
| # Get from: OAuth & Permissions > Bot User OAuth Token | ||
| SLACK_BOT_TOKEN=xoxb-your-token-here | ||
|
|
||
| # Slack App Token for Socket Mode (xapp-...) | ||
| # Get from: Settings > Basic Information > App-Level Tokens | ||
| # Needs connections:write scope | ||
| SLACK_APP_TOKEN=xapp-your-token-here |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| .env | ||
| .venv | ||
| __pycache__/ | ||
| *.pyc |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # Slack Bot | ||
|
|
||
| A basic Slack bot using Socket Mode with a web chat interface. | ||
|
|
||
| The bot here just mirrors back your last word as a question, to demonstrate that it's a good listener. Customize the behavior in `app/responder.py` to do whatever you'd like. | ||
|
|
||
| ## Routes | ||
|
|
||
| - `/` - Web chat interface (Shiny) | ||
| - `/api/health` - Health check endpoint | ||
|
|
||
| ## Slack App Setup | ||
|
|
||
| 1. Create a new app at https://api.slack.com/apps (From scratch) | ||
|
|
||
| 2. **Enable Socket Mode** | ||
| - Settings → Socket Mode → Enable | ||
| - Create an App-Level Token with `connections:write` scope | ||
| - Save this token as `SLACK_APP_TOKEN` (starts with `xapp-`) | ||
|
|
||
| 3. **Add Bot Token Scopes** (OAuth & Permissions → Scopes → Bot Token Scopes) | ||
| - `app_mentions:read` - receive @-mention events | ||
| - `chat:write` - send messages | ||
| - `im:history` - receive DM messages | ||
| - `im:read` - access DM channel info | ||
| - `im:write` - send DMs | ||
|
|
||
| 4. **Subscribe to Events** (Event Subscriptions → Subscribe to bot events) | ||
| - `app_mention` - when someone @-mentions the bot | ||
| - `message.im` - DMs to the bot | ||
|
|
||
| 5. **Install to Workspace** | ||
| - OAuth & Permissions → Install to Workspace | ||
| - Copy the Bot User OAuth Token as `SLACK_BOT_TOKEN` (starts with `xoxb-`) | ||
|
|
||
| ## Running Locally | ||
|
|
||
| ```bash | ||
| # Install dependencies | ||
| uv sync | ||
|
|
||
| # Configure environment | ||
| cp .env.example .env | ||
| # Edit .env with your tokens | ||
|
|
||
| # Run | ||
| uv run uvicorn app.main:app --reload | ||
| ``` | ||
|
|
||
| ## Deploying to Posit Connect | ||
|
|
||
| Set `min_processes` to 1 in the runtime settings so the app stays running and listens for Slack messages. Otherwise, the app will go idle and stop responding to Slack. | ||
|
|
||
| ## Usage | ||
|
|
||
| **Web chat:** | ||
| - Open http://localhost:8000 in your browser | ||
|
|
||
| **Slack:** | ||
| - Mention the bot in a channel: `@bot I feel sad` → `Sad?` | ||
| - DM the bot: `My dog is sick` → `Sick?` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import re | ||
|
|
||
| from fastapi import FastAPI | ||
| from slack_bolt.async_app import AsyncApp | ||
| from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler | ||
|
|
||
| from config import settings | ||
| from responder import generate_response | ||
|
|
||
| # Initialize Slack app (started by main.py lifespan) | ||
| slack_app = AsyncApp(token=settings.slack_bot_token) | ||
|
|
||
|
|
||
| def create_socket_handler() -> AsyncSocketModeHandler: | ||
| """Create the Socket Mode handler for Slack.""" | ||
| return AsyncSocketModeHandler(slack_app, settings.slack_app_token) | ||
|
|
||
|
|
||
| def strip_bot_mention(text: str) -> str: | ||
| """Remove bot @-mention from message text.""" | ||
| return re.sub(r"<@[A-Z0-9]+>\s*", "", text).strip() | ||
|
|
||
|
|
||
| # Slack event handlers | ||
|
|
||
| @slack_app.event("app_mention") | ||
| async def handle_mention(event, say, client): | ||
| """Handle @-mentions in channels and threads.""" | ||
| thread_ts = event.get("thread_ts") or event["ts"] | ||
| user_message = strip_bot_mention(event["text"]) | ||
|
|
||
| response = generate_response(user_message) | ||
| await say(text=response, thread_ts=thread_ts) | ||
|
|
||
|
|
||
| @slack_app.event("message") | ||
| async def handle_dm(event, say, client): | ||
| """Handle DMs to the bot.""" | ||
| # Only handle DMs | ||
| if event.get("channel_type") != "im": | ||
| return | ||
|
|
||
| # Ignore bot messages and subtypes (edits, etc.) | ||
| if event.get("bot_id") or event.get("subtype"): | ||
| return | ||
|
|
||
| user_message = event["text"] | ||
|
|
||
| response = generate_response(user_message) | ||
| await say(text=response) | ||
|
|
||
|
|
||
| # FastAPI app (just health check) | ||
| fastapi_app = FastAPI() | ||
|
|
||
|
|
||
| @fastapi_app.get("/health") | ||
| async def health(): | ||
| """Health check endpoint.""" | ||
| return {"status": "healthy"} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| import os | ||
|
|
||
| from shiny.express import ui | ||
| from shinychat.express import Chat | ||
| from slack_sdk import WebClient | ||
|
|
||
| from responder import generate_response | ||
|
|
||
| # Check for Slack tokens | ||
| slack_bot_token = os.environ.get("SLACK_BOT_TOKEN") | ||
| slack_app_token = os.environ.get("SLACK_APP_TOKEN") | ||
| slack_configured = bool(slack_bot_token and slack_app_token) | ||
|
|
||
| # Get bot name if configured | ||
| bot_name = None | ||
| if slack_configured: | ||
| try: | ||
| client = WebClient(token=slack_bot_token) | ||
| auth_response = client.auth_test() | ||
| bot_name = auth_response.get("user") | ||
| except Exception: | ||
| pass | ||
|
|
||
| ui.page_opts(fillable=True, title="Slack Bot") | ||
|
|
||
| with ui.sidebar(width=350): | ||
| ui.h3("Slack Bot") | ||
|
|
||
| ui.markdown( | ||
| """ | ||
| This is a basic Slack bot with a web chat interface. | ||
|
|
||
| The bot mirrors back your last word as a question, demonstrating | ||
| that it's a good listener. Customize the behavior in | ||
| `responder.py` to do whatever you'd like. | ||
|
|
||
| **Usage:** | ||
| - Chat here in the web interface | ||
| - @mention the bot in Slack | ||
| - DM the bot directly | ||
| """ | ||
| ) | ||
|
|
||
| if slack_configured: | ||
| status_text = f"Slack connected as @{bot_name}" if bot_name else "Slack connected" | ||
| ui.div( | ||
| ui.tags.span(status_text, style="color: green; font-weight: bold;"), | ||
| style="margin: 1em 0; padding: 0.5em; background: #e8f5e9; border-radius: 4px;", | ||
| ) | ||
| else: | ||
| ui.div( | ||
| ui.markdown( | ||
| """ | ||
| **Slack not configured** | ||
|
|
||
| Set these environment variables: | ||
| - `SLACK_BOT_TOKEN` (xoxb-...) | ||
| - `SLACK_APP_TOKEN` (xapp-...) | ||
| """ | ||
| ), | ||
| style="margin: 1em 0; padding: 0.5em; background: #ffebee; border-radius: 4px;", | ||
| ) | ||
|
|
||
| ui.HTML( | ||
| """ | ||
| <details> | ||
| <summary style="cursor: pointer; font-weight: bold; margin-top: 1em;">Posit Connect</summary> | ||
| <p style="margin-top: 0.5em;">Set <code>min_processes</code> to 1 in runtime settings so the app stays running and listens for Slack messages.</p> | ||
| </details> | ||
|
|
||
| <details> | ||
| <summary style="cursor: pointer; font-weight: bold; margin-top: 1em;">Slack App Setup</summary> | ||
|
|
||
| <ol style="padding-left: 1.2em; margin-top: 0.5em;"> | ||
| <li><p>Create a new app at <a href="https://api.slack.com/apps" target="_blank">api.slack.com/apps</a> (From scratch)</p></li> | ||
|
|
||
| <li><p><strong>Enable Socket Mode</strong></p> | ||
| <ul> | ||
| <li>Settings → Socket Mode → Enable</li> | ||
| <li>Create an App-Level Token with <code>connections:write</code> scope</li> | ||
| <li>Save as <code>SLACK_APP_TOKEN</code> (starts with <code>xapp-</code>)</li> | ||
| </ul></li> | ||
|
|
||
| <li><p><strong>Add Bot Token Scopes</strong> (OAuth & Permissions → Bot Token Scopes)</p> | ||
| <ul> | ||
| <li><code>app_mentions:read</code></li> | ||
| <li><code>chat:write</code></li> | ||
| <li><code>im:history</code></li> | ||
| <li><code>im:read</code></li> | ||
| <li><code>im:write</code></li> | ||
| </ul></li> | ||
|
|
||
| <li><p><strong>Subscribe to Events</strong> (Event Subscriptions → Subscribe to bot events)</p> | ||
| <ul> | ||
| <li><code>app_mention</code></li> | ||
| <li><code>message.im</code></li> | ||
| </ul></li> | ||
|
|
||
| <li><p><strong>Install to Workspace</strong></p> | ||
| <ul> | ||
| <li>OAuth & Permissions → Install to Workspace</li> | ||
| <li>Copy Bot User OAuth Token as <code>SLACK_BOT_TOKEN</code> (starts with <code>xoxb-</code>)</li> | ||
| </ul></li> | ||
| </ol> | ||
| </details> | ||
| """ | ||
| ) | ||
|
|
||
| chat = Chat(id="chat") | ||
|
|
||
| chat.ui( | ||
| messages=[ | ||
| {"content": "Hello! How can I help you?", "role": "assistant"}, | ||
| ], | ||
| ) | ||
|
|
||
|
|
||
| @chat.on_user_submit | ||
| async def handle_user_input(user_input: str): | ||
| response = generate_response(user_input) | ||
| await chat.append_message(response) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| from pydantic_settings import BaseSettings | ||
|
|
||
|
|
||
| class Settings(BaseSettings): | ||
| # Slack tokens | ||
| slack_bot_token: str # xoxb-... | ||
| slack_app_token: str # xapp-... (for Socket Mode) | ||
|
|
||
| class Config: | ||
| env_file = ".env" | ||
|
|
||
|
|
||
| settings = Settings() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import asyncio | ||
| from contextlib import asynccontextmanager | ||
| from pathlib import Path | ||
|
|
||
| from starlette.applications import Starlette | ||
| from starlette.routing import Mount | ||
| from shiny.express import wrap_express_app | ||
|
|
||
| from api import fastapi_app, create_socket_handler | ||
|
|
||
| # Wrap the Shiny Express app for mounting | ||
| chat_ui_path = Path(__file__).parent / "chat_ui.py" | ||
| shiny_app = wrap_express_app(chat_ui_path) | ||
|
|
||
|
|
||
| @asynccontextmanager | ||
| async def lifespan(app): | ||
| """Start Slack Socket Mode handler on startup.""" | ||
| handler = create_socket_handler() | ||
| asyncio.create_task(handler.start_async()) | ||
| print("Slack bot started!") | ||
|
|
||
| yield | ||
|
|
||
| await handler.close_async() | ||
|
|
||
|
|
||
| routes = [ | ||
| Mount("/api", app=fastapi_app), | ||
| Mount("/", app=shiny_app), | ||
| ] | ||
|
|
||
| app = Starlette(routes=routes, lifespan=lifespan) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| [project] | ||
| name = "slack-bot" | ||
| version = "0.1.0" | ||
| description = "A Slack bot using Socket Mode" | ||
| requires-python = ">=3.11" | ||
| dependencies = [ | ||
| "fastapi>=0.109.0", | ||
| "uvicorn>=0.27.0", | ||
| "slack-bolt>=1.18.0", | ||
| "aiohttp>=3.9.0", | ||
| "pydantic-settings>=2.1.0", | ||
| "shiny>=1.0.0", | ||
| "shinychat>=0.1.0", | ||
| ] | ||
|
|
||
| [project.optional-dependencies] | ||
| dev = [ | ||
| "pytest>=8.0.0", | ||
| "pytest-asyncio>=0.23.0", | ||
| ] | ||
|
|
||
| [build-system] | ||
| requires = ["hatchling"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [tool.hatch.build.targets.wheel] | ||
| packages = ["app"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| """Message response generation.""" | ||
|
|
||
|
|
||
| def generate_response(message: str, context: list[dict] | None = None) -> str: | ||
| """Generate a response to a user message. | ||
|
|
||
| Args: | ||
| message: The user's message text. | ||
| context: Optional conversation history (for future use). | ||
|
|
||
| Returns: | ||
| The response text. | ||
| """ | ||
| return _get_last_word_question(message) | ||
|
|
||
|
|
||
| def _get_last_word_question(text: str) -> str: | ||
| """Extract the last word and return it as a question.""" | ||
| words = text.strip().rstrip("?!.,").split() | ||
| if words: | ||
| return f"{words[-1].capitalize()}?" | ||
| return "Hmm?" |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For DMs to the bot to work, I had to also ensure App Home -> "Allow users to send Slash commands and messages from the messages tab" was ticked. (It was unchecked by default.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will revise the readme with all of the relevant scopes needed, Claude's first guess was wrong, but I've figured it out since. I don't believe slash messages is required but there are more bot scopes needed than were originally figured.