A safe, one-shot command-line tool that adds or removes a Discord role for every non-bot user who reacted to a specific message or voted in its native Discord poll. It logs in, performs one synchronization, prints a per-user audit log, and exits. It is not a continuously running bot.
- Supports
ADDandREMOVEactions. - Reads classic message reactions, native Discord poll votes, or both.
- Deduplicates users who selected multiple reactions or poll answers.
- Handles users who already have (or do not have) the role without failing.
- Continues processing after an individual member error.
- Supports a no-change
--dry-runsafety mode. - Keeps the bot token in environment variables and never prints it.
- Scans readable text channels when a channel ID is not supplied.
- Includes automated tests for configuration, collection, idempotency, dry-run, member fetching, and partial failure behavior.
- Python 3.11 or newer.
- A Discord application with a bot user.
- The bot invited to the target server with:
- View Channels
- Read Message History
- Manage Roles
- The bot's role placed above the role it will add or remove.
The program only reads existing message participation and edits the configured role. It does not read or respond to ordinary chat content.
git clone <your-repository-url>
cd discord-role-sync
python -m venv .venvActivate the virtual environment:
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell
.venv\Scripts\Activate.ps1Install the package:
python -m pip install -e .- Open the Discord Developer Portal and create an application.
- Add a bot to the application and copy its token.
- In OAuth2 > URL Generator, select the
botscope and the three permissions listed above, then use the generated URL to invite it. - In Server Settings > Roles, move the bot role above the target role.
- Enable Developer Mode in Discord under User Settings > Advanced.
- Right-click the server, message, role, and (recommended) channel to copy IDs.
Do not commit or share the bot token. If it is exposed, reset it immediately in the Developer Portal.
Copy the example file and insert real values:
cp .env.example .envRequired values:
DISCORD_BOT_TOKEN=replace_with_your_bot_token
DISCORD_SERVER_ID=123456789012345678
DISCORD_MESSAGE_ID=123456789012345678
DISCORD_ROLE_ID=123456789012345678
DISCORD_ACTION=ADDRecommended and optional values:
DISCORD_CHANNEL_ID=123456789012345678
DISCORD_SOURCE=auto
DISCORD_DRY_RUN=falseDISCORD_SOURCE accepts:
auto: combine reaction users and native poll voters.reactions: only users attached to message reactions.poll: only voters from native Discord poll answers.
When DISCORD_CHANNEL_ID is omitted, the program searches the server's
readable text channels for the configured message. Supplying it is faster and
avoids unnecessary API requests.
First perform a dry run:
discord-role-sync --dry-runThen apply the configured action:
discord-role-syncCLI flags can override IDs, action, or source without editing .env:
discord-role-sync \
--server-id 123456789012345678 \
--channel-id 123456789012345678 \
--message-id 123456789012345678 \
--role-id 123456789012345678 \
--action REMOVE \
--source reactionsThe token intentionally has no CLI flag so it is less likely to leak through shell history or process listings.
2026-08-15T12:00:00Z INFO MATCHED users=3 source=auto message=123456789012345678
2026-08-15T12:00:00Z INFO SUCCESS action=ADD user=alice id=111 role=999
2026-08-15T12:00:00Z INFO SKIP user=bob id=222 reason=already_has_role
2026-08-15T12:00:01Z INFO SUCCESS action=ADD user=carol id=333 role=999
2026-08-15T12:00:01Z INFO SUMMARY matched=3 changed=2 would_change=0 skipped=1 failed=0 dry_run=False
Exit codes:
0: run completed with no per-user failures.1: Discord/runtime error or one or more member updates failed.2: invalid or missing configuration.130: interrupted by the operator.
python -m pip install -e '.[dev]'
pytest
ruff check .Tests use local fakes and never contact Discord.
- A user appearing in several reactions or poll answers is changed once.
- Bot users are ignored.
ADDskips users who already have the role.REMOVEskips users who do not have the role.- An individual API failure is logged while remaining users continue.
- Discord rate limiting is handled by
discord.py. - Native poll voter retrieval requires
discord.py2.5 or newer.
See DEMO.md for a concise end-to-end demonstration checklist.