Reaction roles (Python)
Gives members a role when they react to a message with the matching emoji and takes it away when they remove the reaction.
Python 3.13
♥ 0 likes
Environment variables you'll need
| Key | What it is |
|---|---|
DISCORD_TOKEN | Your bot's token from the Discord Developer Portal. Paste it on the Secrets tab; we store it encrypted. |
REACTION_ROLES | Pairs of emoji=role id, separated by commas, e.g. 👍=123456789,🎮=987654321 |
ROLE_MESSAGE_ID | The message to watch. /reactionroles-setup posts one and prints its id |
Files
README.md
# Reaction roles
A Python bot built on discord.py. Your bot token is set on the Secrets tab as
DISCORD_TOKEN and reaches the bot as an environment variable.
## Commands
- `/reactionroles-setup` (needs Manage Roles) posts a message listing the pairs, adds the reactions and prints the message id. Put that id into ROLE_MESSAGE_ID on the Secrets tab and restart the bot.
- Custom emoji work by name or id, e.g. `party=123` or `<:party:111>=123`.
- The bot's highest role must be above every role it hands out.
## Setup
1. No privileged gateway intents are needed. Leave Message Content, Server Members
and Presence off in the Discord Developer Portal.
2. Invite the bot with the `bot` and `applications.commands` scopes and these
permissions: Manage Roles, Add Reactions, Send Messages, Read Message History.
3. Set these values on the Secrets tab. The ones marked optional can stay empty:
- `REACTION_ROLES`: Pairs of emoji=role id, separated by commas, e.g. 👍=123456789,🎮=987654321.
- `ROLE_MESSAGE_ID`: The message to watch. /reactionroles-setup posts one and prints its id.
4. Press Deploy. The console shows a line with "Logged in as" once the bot is online.
main.py
import os
import re
import discord
from discord import app_commands
CUSTOM_EMOJI = re.compile(r"^<a?:(\w+):(\d+)>$")
def normalize(key):
"""Custom emoji written as <:name:id> become their id; unicode loses FE0F."""
key = key.strip()
match = CUSTOM_EMOJI.match(key)
if match:
return match.group(2)
return key.replace(chr(0xFE0F), "")
def parse_pairs(raw):
pairs = {}
for item in raw.split(","):
if "=" not in item:
continue
emoji, role_id = item.rsplit("=", 1)
role_id = role_id.strip()
if emoji.strip() and role_id.isdigit():
pairs[normalize(emoji)] = (emoji.strip(), int(role_id))
else:
print(f"Skipping bad REACTION_ROLES entry: {item!r}")
return pairs
MESSAGE_VALUE = os.environ.get("ROLE_MESSAGE_ID", "").strip()
ROLE_MESSAGE_ID = int(MESSAGE_VALUE) if MESSAGE_VALUE.isdigit() else None
PAIRS = parse_pairs(os.environ.get("REACTION_ROLES", ""))
def role_for(emoji):
"""Match a reaction by custom emoji id, custom emoji name, or unicode text."""
keys = [str(emoji.id)] if emoji.id else []
keys.append(normalize(emoji.name or ""))
for key in keys:
if key in PAIRS:
return PAIRS[key][1]
return None
class RoleBot(discord.Client):
def __init__(self):
super().__init__(intents=discord.Intents.default())
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
await self.tree.sync()
bot = RoleBot()
async def update_role(payload, add):
if payload.guild_id is None or payload.message_id != ROLE_MESSAGE_ID:
return
role_id = role_for(payload.emoji)
if role_id is None:
return
guild = bot.get_guild(payload.guild_id)
if guild is None:
return
role = guild.get_role(role_id)
if role is None:
print(f"Role {role_id} for {payload.emoji} does not exist in {guild.name}")
return
try:
member = payload.member or guild.get_member(payload.user_id) \
or await guild.fetch_member(payload.user_id)
if member.bot:
return
if add:
await member.add_roles(role, reason="Reaction role")
else:
await member.remove_roles(role, reason="Reaction role")
except discord.NotFound:
return
except discord.HTTPException as exc:
print(f"Could not update role {role.name}: {exc}. Is my role above it?")
@bot.event
async def on_raw_reaction_add(payload):
await update_role(payload, True)
@bot.event
async def on_raw_reaction_remove(payload):
await update_role(payload, False)
def resolve_emoji(guild, key, original):
"""Return something add_reaction accepts: a guild Emoji or a unicode string."""
if key.isdigit():
return bot.get_emoji(int(key))
found = discord.utils.get(guild.emojis, name=key)
return found or original
@bot.tree.command(name="reactionroles-setup",
description="Post the reaction roles message in this channel")
@app_commands.default_permissions(manage_roles=True)
@app_commands.guild_only()
async def setup(interaction: discord.Interaction):
if not PAIRS:
await interaction.response.send_message(
"REACTION_ROLES is empty. Set it in the Secrets tab first.", ephemeral=True)
return
# posting and reacting can take longer than the 3 second reply window
await interaction.response.defer(ephemeral=True)
guild = interaction.guild
lines = ["React to get a role. Remove your reaction to lose it."]
emojis = []
for key, (original, role_id) in PAIRS.items():
emoji = resolve_emoji(guild, key, original)
emojis.append(emoji)
role = guild.get_role(role_id)
label = role.mention if role else f"missing role {role_id}"
lines.append(f"{emoji or original} : {label}")
message = await interaction.channel.send(
"\n".join(lines), allowed_mentions=discord.AllowedMentions
requirements.txt
discord.py