[Bot] Drop the unused privileged message_content intent and stop restart-looping on a missing Developer Portal toggle #394

Closed
opened 2026-08-25 20:42:30 +00:00 by claude-bot · 2 comments
Contributor

Impact: HIGH

Found in the August 2026 session lifecycle review (#319).

What the user experiences

The bot requests the privileged message_content intent on startup but never uses it — there is no on_message handler and no prefix commands anywhere in the codebase. A customer who misses the corresponding toggle in the Discord Developer Portal gets PrivilegedIntentsRequired, which is not a discord.LoginFailure and so is not caught by the bot's existing startup error handling — the process crashes and the Compose restart policy loops it indefinitely. The requirement to enable this toggle is documented only in a code comment, nowhere in docs/.

Evidence

  • bot/questboard_bot/main.py:122-124intents.message_content = True, a privileged intent; grep for on_message and prefix-command usage in bot/ finds none.
  • bot/questboard_bot/main.py:407-421 — the startup error handler catches discord.LoginFailure but not PrivilegedIntentsRequired, so the process crashes uncaught.
  • bot/questboard_bot/config.py:461-463 — the Developer Portal toggle requirement exists only as a code comment, not in docs/.

Why it matters for a hosted product

This is a request for a permission the bot doesn't need, and the failure mode when a customer misses it is a silent crash-restart loop rather than a clear error — the worst possible combination for a self-hoster trying to diagnose why their bot won't start.

Proposed fix

Drop intents.message_content = True since nothing in the codebase uses it. If it's needed for a planned future feature, keep it but catch PrivilegedIntentsRequired explicitly in the startup handler (main.py:417) and log an actionable message naming the Developer Portal toggle, instead of crash-looping. Document the requirement in docs/ if the intent is kept. This is the audit's P30.

Acceptance criteria

  • message_content intent is removed, or a concrete planned use is documented alongside keeping it.
  • If kept, PrivilegedIntentsRequired is caught at startup with an actionable log message naming the missing toggle.
  • The bot no longer crash-restart-loops silently on this specific misconfiguration.
  • If the intent is kept, its requirement is documented in docs/, not only in a code comment.
**Impact: HIGH** Found in the August 2026 session lifecycle review (#319). ## What the user experiences The bot requests the privileged `message_content` intent on startup but never uses it — there is no `on_message` handler and no prefix commands anywhere in the codebase. A customer who misses the corresponding toggle in the Discord Developer Portal gets `PrivilegedIntentsRequired`, which is not a `discord.LoginFailure` and so is not caught by the bot's existing startup error handling — the process crashes and the Compose restart policy loops it indefinitely. The requirement to enable this toggle is documented only in a code comment, nowhere in `docs/`. ## Evidence - `bot/questboard_bot/main.py:122-124` — `intents.message_content = True`, a privileged intent; grep for `on_message` and prefix-command usage in `bot/` finds none. - `bot/questboard_bot/main.py:407-421` — the startup error handler catches `discord.LoginFailure` but not `PrivilegedIntentsRequired`, so the process crashes uncaught. - `bot/questboard_bot/config.py:461-463` — the Developer Portal toggle requirement exists only as a code comment, not in `docs/`. ## Why it matters for a hosted product This is a request for a permission the bot doesn't need, and the failure mode when a customer misses it is a silent crash-restart loop rather than a clear error — the worst possible combination for a self-hoster trying to diagnose why their bot won't start. ## Proposed fix Drop `intents.message_content = True` since nothing in the codebase uses it. If it's needed for a planned future feature, keep it but catch `PrivilegedIntentsRequired` explicitly in the startup handler (`main.py:417`) and log an actionable message naming the Developer Portal toggle, instead of crash-looping. Document the requirement in `docs/` if the intent is kept. This is the audit's P30. ## Acceptance criteria - [ ] `message_content` intent is removed, or a concrete planned use is documented alongside keeping it. - [ ] If kept, `PrivilegedIntentsRequired` is caught at startup with an actionable log message naming the missing toggle. - [ ] The bot no longer crash-restart-loops silently on this specific misconfiguration. - [ ] If the intent is kept, its requirement is documented in `docs/`, not only in a code comment.
Author
Contributor

Picking this up as part of v4.3.0 phase 1 (#514), shipping early as v4.2.3. Dropping the intent, since nothing reads message content, and catching PrivilegedIntentsRequired at startup anyway with a message that names the toggle, so a future intent cannot reintroduce the crash-loop.

Picking this up as part of v4.3.0 phase 1 (#514), shipping early as v4.2.3. Dropping the intent, since nothing reads message content, and catching `PrivilegedIntentsRequired` at startup anyway with a message that names the toggle, so a future intent cannot reintroduce the crash-loop.
rbrooks referenced this issue from a commit 2026-09-06 02:00:15 +00:00
Author
Contributor

Fixed in PR #517 (merged), shipping in v4.2.3.

Confirmed by grep before removing anything: no on_message, no @commands.command or bot.command, no hybrid commands anywhere in bot/; all eight commands are @app_commands.command. command_prefix="!" remains only because commands.Bot requires the argument, and with the intent off it can never match. The intent is removed.

PrivilegedIntentsRequired is now caught anyway, for the next intent someone adds and for a portal toggle switched off under a running deployment. It is handled the way LoginFailure already was, for the same reason: both are configuration mistakes no amount of retrying fixes, so the gateway client stops while the aiohttp server keeps serving /health. Deliberately not a process exit: docker-compose.yml sets restart: unless-stopped, which restarts on any exit code including 0, so exiting would crash-loop just as before, only more quietly. This way the container stays up and the log holds one line naming the exact Developer Portal toggle instead of a wall of identical tracebacks. That path moved into _run_discord_bot() so it can be tested without standing up the web app. The trade-off is that /health then reports healthy while Discord is dead, which is the pre-existing trade-off for LoginFailure too; making the healthcheck reflect it would be a separate small issue.

Docs corrected in four places because they were actively wrong: .env.example told operators to enable Message Content and called Server Members "optional, for display names", which is backwards, since Server Members is what makes per-speaker recording attribution possible; also config.py's intents comment, docs/INTEGRATIONS.md's vague "enable the required intents", and a new docs/OPERATIONS.md troubleshooting entry for the symptom.

Four new tests: the constructed intents object has message_content False and members True; _run_discord_bot turns both PrivilegedIntentsRequired and LoginFailure into a logged clean stop naming the toggle; plus the success path. Self-hosters can now turn the Message Content toggle off in the Developer Portal; leaving it on is harmless.

Fixed in PR #517 (merged), shipping in v4.2.3. Confirmed by grep before removing anything: no `on_message`, no `@commands.command` or `bot.command`, no hybrid commands anywhere in `bot/`; all eight commands are `@app_commands.command`. `command_prefix="!"` remains only because `commands.Bot` requires the argument, and with the intent off it can never match. The intent is removed. `PrivilegedIntentsRequired` is now caught anyway, for the next intent someone adds and for a portal toggle switched off under a running deployment. It is handled the way `LoginFailure` already was, for the same reason: both are configuration mistakes no amount of retrying fixes, so the gateway client stops while the aiohttp server keeps serving `/health`. Deliberately **not** a process exit: `docker-compose.yml` sets `restart: unless-stopped`, which restarts on any exit code including 0, so exiting would crash-loop just as before, only more quietly. This way the container stays up and the log holds one line naming the exact Developer Portal toggle instead of a wall of identical tracebacks. That path moved into `_run_discord_bot()` so it can be tested without standing up the web app. The trade-off is that `/health` then reports healthy while Discord is dead, which is the pre-existing trade-off for `LoginFailure` too; making the healthcheck reflect it would be a separate small issue. Docs corrected in four places because they were actively wrong: `.env.example` told operators to enable Message Content and called Server Members "optional, for display names", which is backwards, since Server Members is what makes per-speaker recording attribution possible; also `config.py`'s intents comment, `docs/INTEGRATIONS.md`'s vague "enable the required intents", and a new `docs/OPERATIONS.md` troubleshooting entry for the symptom. Four new tests: the constructed intents object has `message_content` False and `members` True; `_run_discord_bot` turns both `PrivilegedIntentsRequired` and `LoginFailure` into a logged clean stop naming the toggle; plus the success path. Self-hosters can now turn the Message Content toggle off in the Developer Portal; leaving it on is harmless.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#394
No description provided.