Discord Bot
Last updated: 2026-05-26
Overview
The Monstermessenger Discord bot provides a fully integrated chatbot experience through Discord Direct Messages. It authenticates users via the same backend API as the web frontend, supports the full interactive conversation flow (free text + choice buttons), and offers language detection, feedback collection, and monster switching — all within Discord’s UI primitives.
Key characteristics:
- DM-only (ignores server messages)
- Deterministic user identity via Discord ID → UUID
- Automatic consent registration for Discord users
- Per-user in-memory session store (
discord_sessionsdict) - Unified
POST /chatendpoint (no separate emotion/dropdown endpoints) - 6-language i18n for bot-specific UI strings
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Discord Bot │
│ │
│ ┌──────────┐ ┌───────────────┐ ┌─────────────────────────┐ │
│ │ bot.py │ │ bot_feedback │ │ bot_language_choice.py │ │
│ │ (main) │ │ (thumbs/modal)│ │ (LEGACY — deprecated) │ │
│ └────┬─────┘ └───────┬───────┘ └─────────────────────────┘ │
│ │ │ │
│ ┌────┴────┐ ┌──────┴──────┐ │
│ │ config │ │ health_server│ │
│ └────┬────┘ └──────┬──────┘ │
│ │ │ │
│ ┌────┴────┐ ┌──────┴──────┐ │
│ │ i18n.json│ │ Dockerfile │ │
│ └─────────┘ └─────────────┘ │
│ │
└──────────────────────────┬──────────────────────────────────────┘
│ HTTP (REST)
▼
┌─────────────────────────┐
│ FastAPI Backend │
│ ┌──────────────────┐ │
│ │ POST /auth/login │ │
│ │ POST /chat/init │ │
│ │ POST /chat │ │
│ │ POST /feedback/* │ │
│ │ PATCH /auth/profile│ │
│ └──────────────────┘ │
└─────────────────────────┘
File Map
| File | Role |
|---|---|
discord_bot/bot.py |
Main bot — event handlers, auth, chat, interactive prompts, monster switching |
discord_bot/bot_feedback.py |
Feedback UI — thumbs-up/down buttons, text modal, backend submission |
discord_bot/bot_language_choice.py |
LEGACY — pre-unification bot with separate /chat/emotion and /chat/dropdown endpoints. Kept for reference; all active features merged into bot.py |
discord_bot/config.py |
Environment variable loader (DISCORD_BOT_TOKEN, BACKEND_URL, etc.) |
discord_bot/health_server.py |
Lightweight HTTP health check server (:8080), runs in daemon thread |
discord_bot/i18n.json |
Localized strings (6 languages) for monster switch messages and feedback UI |
discord_bot/Dockerfile |
Docker build for python:3.12-slim |
discord_bot/requirements.txt |
Python dependencies |
discord_bot/monsters_emojis/ |
SVG + PNG emoji assets for monster avatars |
Authentication Flow
The bot uses the same mock-IDP authentication pipeline as the web client, but hardcodes certain assumptions suitable for the Discord audience:
# Step 1 — Deterministic UUID from Discord ID
user_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, str(discord_id)))
# Step 2 — Get JWT from mock IDP
POST /api/v1/mock-idp/authenticate {"idp_user_id": user_uuid}
→ {"access_token": "..."}
# Step 3 — Exchange JWT for bearer token
POST /api/v1/auth/login {
"idp_token": "...",
"id_provider": "mock-idp",
"user_type": "teenager",
"age_class": "teenager 15-17", # hardcoded for Discord
"language": "fr", # from --language CLI arg
"origin": "discord",
"capabilities": {"supports_sse": false} # Discord has no SSE
}
→ {"bearer_token": "...", "profile": {...}}Auto-Consent Registration
If the backend response indicates the user has not yet consented (profile.consent_verification.has_consented == false), the bot automatically calls PATCH /api/v1/auth/profile with {"has_consented": true}. This bypasses the consent-gate UI for Discord users (who cannot render the web consent dialog).
Token Caching
The bearer_token is cached in discord_sessions[user_uuid]["bearer_token"] for the lifetime of the process. On restart, tokens are regenerated.
Session Management
All session state is stored in an in-memory Python dictionary:
discord_sessions = {
"<uuid5>": {
"bearer_token": "...",
"session_id": "...",
"monster_name": "betty",
"last_button_msg_id": 123456789
}
}| Field | Set by | Purpose |
|---|---|---|
bearer_token |
get_bearer_token() |
Authenticated API calls |
session_id |
start_discord_session() |
Backend conversation session ID |
monster_name |
send_backend_response() |
Current active monster for emoji display + switching |
last_button_msg_id |
send_backend_response() |
Most recent button message ID — disabled when user sends free text |
Session initialization happens on first DM from a user:
discord_id_to_uuid()converts the Discord user ID to a deterministic UUIDstart_discord_session()callsPOST /chat/initwith{"resume_conversation_id": null}- The session ID from the response is stored
Ephemeral note: Sessions are NOT persisted across bot restarts. On restart, all users start new sessions. For long-running deployments, consider adding a session persistence layer (Redis, etc.).
Chat Flow
Free-Text Messages
When a user sends a text message in DM:
- Disable any active choice buttons from the previous interaction
call_backend_chat()→POST /chatwith{"message": content, "session_id": session_id}send_backend_response()renders the backend reply, checks for monster switch, and creates interactive buttons if present
Feedback System
When the backend sends a message with metadata.action == "user_feedback", the bot attaches a feedbackView to the message:
┌─────────────────────────────────┐
│ betty: Thanks for sharing... │
│ │
│ [👍] [👎] [➕] │
└─────────────────────────────────┘
| Button | Action |
|---|---|
| 👍 | Opens FeedbackModal — user types feedback, is_positive=True |
| 👎 | Opens FeedbackModal — user types feedback, is_positive=False |
| ➕ | Triggers on_new_session callback — reinitializes POST /chat/init for a fresh conversation |
The modal submits to POST /api/v1/feedback/{session_id} with:
{
"session_id": "...",
"user_id": "...",
"value": 10,
"is_positive": true,
"comment": "User's feedback text",
"incident_type": null
}All feedback UI strings are localized (see I18n).
Monster Switching
The bot detects monster changes by comparing the previous monster name (from discord_sessions) with the monster field in the backend response:
previous_monster = session_data.get('monster_name')
current_monster = data.get("monster", DEFAULT_MONSTER)
if previous_monster != current_monster:
await monster_switch(channel, previous_monster, current_monster)
session_data['monster_name'] = current_monsterThe switch message uses a localized template: "{monster} will help you" (English) / "{monster} va t'aider" (French).
Emoji Display
Each message is prefixed with the monster’s Discord emoji (if available on any server the bot is in):
monster_emoji = discord.utils.get(client.emojis, name=monster_name)
prefix = f"{monster_emoji} **{monster_name}:**" if monster_emoji else f"**{monster_name}:**"Emoji assets (betty.svg, emoji_betty.png) are in discord_bot/monsters_emojis/. To add a new monster, upload its emoji to a server the bot is a member of, then reference it by name.
Language Detection
The bot auto-detects the user’s language on their first message using the langid library:
from langid import classify
def detect_language(text: str):
lang = classify(text)[0].upper()
return lang if lang in SUPPORTED_LANGS else DEFAULT_LANGUAGE
SUPPORTED_LANGS = ['DE', 'EN', 'ES', 'FR', 'IT', 'PT']The detected language is sent in the POST /auth/login payload ("language": lang.lower()). After the first message, detection_lang is set to False and the language is locked for the session.
CLI Language Override
The bot accepts a --language / -l CLI argument for a default language:
python bot.py -l FR # French default
python bot.py -l EN # English defaultThe Dockerfile defaults to French: CMD ["python", "bot.py", "-l", "FR"]
I18n
Bot-specific UI strings (monster switch message, feedback labels) are stored in i18n.json with 6 language keys (FR, EN, ES, DE, IT, PT). Each language provides:
| Key | Used in |
|---|---|
monster_switch |
Monster switching message template |
feedback_label |
Feedback modal title |
feedback_placeholder |
Feedback modal placeholder text |
feedback_submit |
Ephemeral confirmation after feedback submission |
Example (French):
{
"FR": {
"monster_switch": "{monster} va t'aider",
"feedback_label": "Ton retour",
"feedback_placeholder": "Donne ton avis",
"feedback_submit": "Merci pour ton retour ! Tu peux continuer la conversation maintenant"
}
}Configuration
All configuration is via environment variables, loaded in config.py:
| Variable | Default | Description |
|---|---|---|
DISCORD_BOT_TOKEN |
(required) | Discord application bot token |
BACKEND_URL |
http://localhost:8000/api/v1 |
Backend API base URL (with /api/v1) |
DEFAULT_MONSTER |
betty |
Default monster character name |
DEFAULT_LANGUAGE |
EN |
Fallback language code |
Docker Deployment
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "bot.py", "-l", "FR"]Build and run:
docker build -t discord-bot -f discord_bot/Dockerfile discord_bot/
docker run -e DISCORD_BOT_TOKEN="..." -e BACKEND_URL="https://..." discord-botNote: The bot runs as a single long-lived process. For production, consider running it behind a process manager (systemd, supervisord) or deploying to a cloud VM.
Health Server
A lightweight HTTP health check server runs on port 8080 in a daemon thread:
HealthServer().start() # starts before client.run()The server responds 200 OK with body OK to any GET request. This is useful for: - Kubernetes/Docker health probes - Cloud load balancer health checks - Monitoring uptime
Error Handling
The @handle_errors decorator wraps all async functions, catching exceptions and sending a user-facing error message:
@handle_errors
async def call_backend_chat(...):
...On failure, the user sees: "An error occurred while processing your request". Errors are logged to stdout with [ERROR] function_name failed: exception_message.
If the decorator cannot find a channel to send to, it falls back to printing the error.
Comparison: bot.py vs bot_language_choice.py
| Feature | bot.py (current) |
bot_language_choice.py (legacy) |
|---|---|---|
| Chat endpoint | POST /chat (unified) |
POST /chat, POST /chat/emotion, POST /chat/dropdown |
| Interactive prompts | Unified InteractivePrompt with prompt_style |
Separate emotion_options / dropdown_options |
| Auth | Mock-IDP → bearer token with consent auto-registration | Direct user_id in request body (no auth) |
| Monster support | All monsters via emoji name match | Betty only (hardcoded) |
| Language | langid detection + --language override |
langid detection only |
| Feedback | Full feedbackView with thumbs + modal + new session | Basic feedbackView (no new session button) |
| Error handling | @handle_errors decorator with recovery |
@handle_errors with recovery |
| Health server | Included | Not included |
| Button UX | Row-of-4 layout, green highlight, disable-on-text | Row-of-4 for emotions only, no disable-on-text |
bot_language_choice.py is deprecated and kept for reference only. All active development targets bot.py.
Limitations & Future Work
- No session persistence: Sessions are lost on bot restart. Consider Redis or database-backed session store.
- No SSE streaming: Responses are delivered as complete messages (no token-by-token streaming like the web frontend).
- Single monster emoji per name: If multiple servers have different emoji versions, only the first found is used.
- Hardcoded age class: All users are assumed
"teenager 15-17". Future versions could ask the user. - No rich embeds: Messages use plain text with emoji prefixes. Discord Embeds could provide richer formatting.