Skip to content

One-shot REST commands unnecessarily open Gateway sessions and require privileged intents #24

Description

@doggy8088

Summary

A one-shot command that only needs the Discord REST API currently opens a Gateway connection and requests every intent, including all privileged intents.

I encountered this while trying to inspect the reactions on one known message:

export DISCORD_BOT_TOKEN="<valid bot token>"
uv run discli reaction list <channel_id> <message_id>

The channel ID and message ID were valid, and the token authenticated successfully. However, the command failed before it attempted to fetch the message because the application did not have every privileged Gateway intent enabled.

Environment

  • macOS
  • discord-cli-agent==0.9.1
  • Python 3.12
  • discord.py>=2.7.1
  • discli run from the repository with uv run
  • The same bot token was already being used by a separate, long-running bot process with its own Gateway session

Actual behavior

The command fails with:

discord.errors.PrivilegedIntentsRequired:
Shard ID None is requesting privileged intents that have not been
explicitly enabled in the developer portal.

The traceback points to run_action(), where discli constructs a client with all intents and starts a Gateway connection:

intents = discord.Intents.all()
client = discord.Client(intents=intents)
...
await client.start(token)

Source:

The reaction command itself only does the following after the Gateway becomes ready:

ch = resolve_channel(client, channel)
msg = await ch.fetch_message(int(message_id))
reactions = [
    {"emoji": str(reaction.emoji), "count": reaction.count}
    for reaction in msg.reactions
]

The message is never fetched because Discord rejects the unnecessary Gateway IDENTIFY first. The failed startup also prints an Unclosed connector warning because the client is not closed when client.start() fails before on_ready.

Expected behavior

reaction list should use the REST API without opening a Gateway WebSocket.

Fetching one known message is supported by:

GET /channels/{channel.id}/messages/{message.id}
Authorization: Bot <token>

For a guild channel, Discord documents VIEW_CHANNEL and READ_MESSAGE_HISTORY as the required permissions. Reading the message's reaction summary does not require Presence, Server Members, Message Content, or reaction event intents.

The command should therefore work when:

  • the bot token is valid;
  • the bot can view the channel; and
  • the bot has permission to read message history.

It should not require users to enable unrelated privileged intents.

Analysis

The behavior appears to come from using one shared Gateway-based runner for both one-shot REST commands and long-running event consumers.

discord.Client.start() performs login and then connects to the Gateway. Because the runner uses discord.Intents.all(), every invocation sends an IDENTIFY requesting GUILD_PRESENCES, GUILD_MEMBERS, and MESSAGE_CONTENT, even if the command only performs an HTTP request.

This has several consequences:

  1. REST-only commands fail unless unrelated privileged intents are enabled.
  2. Users are encouraged to grant sensitive access that the requested operation does not need.
  3. Every CLI invocation creates an unnecessary short-lived Gateway connection and IDENTIFY attempt.
  4. This adds avoidable session-start pressure for applications that already have a long-running Gateway bot.
  5. Discord's Gateway documentation limits clients to 1,000 IDENTIFY calls per 24 hours across the application; reaching that limit terminates active sessions and resets the bot token.
  6. Resource resolution currently uses cache-only methods such as client.get_channel(), which unnecessarily couples otherwise REST-capable commands to Gateway-populated state.

This is broader than reaction list: other one-shot commands using the same run_discord() path also open a Gateway session before performing REST operations.

Suggested direction

A reasonable separation would be:

  • Use an HTTP-only lifecycle for one-shot commands: authenticate with Client.login(), resolve resources with REST fetch methods such as fetch_channel(), perform the action, and close the client in finally.
  • Reserve Client.start() and Gateway sessions for commands that actually consume real-time events or voice state, such as listen, serve, and voice functionality.
  • For Gateway commands, request only the intents required by the selected features instead of Intents.all().
  • Handle PrivilegedIntentsRequired explicitly and always close the client on startup failures.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions