Reaction roles (JavaScript)
Gives members a role when they react to a message with the matching emoji and takes it away when they remove the reaction.
Node.js 22
♥ 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 JavaScript bot built on discord.js. 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.
index.js
const {
Client, Events, GatewayIntentBits, MessageFlags, Partials, PermissionFlagsBits, SlashCommandBuilder,
} = require('discord.js');
const CUSTOM_EMOJI = /^<a?:(\w+):(\d+)>$/;
const VARIATION_SELECTOR = /\uFE0F/g;
// Custom emoji written as <:name:id> become their id; unicode emoji lose U+FE0F.
function normalize(key) {
const trimmed = key.trim();
const match = trimmed.match(CUSTOM_EMOJI);
return match ? match[2] : trimmed.replace(VARIATION_SELECTOR, '');
}
function parsePairs(raw) {
const pairs = new Map();
for (const item of raw.split(',')) {
const at = item.lastIndexOf('=');
if (at < 0) continue;
const emoji = item.slice(0, at).trim();
const roleId = item.slice(at + 1).trim();
if (emoji && /^\d+$/.test(roleId)) pairs.set(normalize(emoji), { original: emoji, roleId });
else console.log(`Skipping bad REACTION_ROLES entry: ${item}`);
}
return pairs;
}
const ROLE_MESSAGE_ID = (process.env.ROLE_MESSAGE_ID || '').trim();
const PAIRS = parsePairs(process.env.REACTION_ROLES || '');
// Match a reaction by custom emoji id, custom emoji name, or unicode text.
function roleFor(emoji) {
for (const key of [emoji.id, emoji.name && normalize(emoji.name)]) {
if (key && PAIRS.has(key)) return PAIRS.get(key).roleId;
}
return null;
}
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessageReactions],
// Partials let the bot see reactions on messages sent before it started.
partials: [Partials.Message, Partials.Channel, Partials.Reaction, Partials.User],
});
const commands = [
new SlashCommandBuilder().setName('reactionroles-setup')
.setDescription('Post the reaction roles message in this channel')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles)
.toJSON(),
];
async function updateRole(reaction, user, add) {
if (!ROLE_MESSAGE_ID || reaction.message.id !== ROLE_MESSAGE_ID) return;
const roleId = roleFor(reaction.emoji);
const guild = reaction.message.guild;
if (!roleId || !guild) return;
const role = guild.roles.cache.get(roleId);
if (!role) {
console.log(`Role ${roleId} for ${reaction.emoji.name} does not exist in ${guild.name}`);
return;
}
try {
const member = await guild.members.fetch(user.id);
if (member.user.bot) return;
if (add) await member.roles.add(role, 'Reaction role');
else await member.roles.remove(role, 'Reaction role');
} catch (err) {
console.error(`Could not update role ${role.name}: ${err.message}. Is my role above it?`);
}
}
client.on(Events.MessageReactionAdd, (reaction, user) => updateRole(reaction, user, true));
client.on(Events.MessageReactionRemove, (reaction, user) => updateRole(reaction, user, false));
// Returns something message.react() accepts: a guild emoji, an id, or unicode text.
function resolveEmoji(guild, key, original) {
if (/^\d+$/.test(key)) return guild.emojis.cache.get(key) || client.emojis.cache.get(key) || null;
return guild.emojis.cache.find((e) => e.name === key) || original;
}
async function setup(interaction) {
if (PAIRS.size === 0) {
await interaction.reply({ content: 'REACTION_ROLES is empty. Set it in the Secrets tab first.', flags: MessageFlags.Ephemeral });
return;
}
// posting and reacting can take longer than the 3 second reply window
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const { guild } = interaction;
const lines = ['React to get a role. Remove your reaction to lose it.'];
const emojis = [];
for (const [key, { original, roleId }] of PAIRS) {
const emoji = resolveEmoji(guild, key, original);
emojis.push([emoji, original]);
const role = guild.roles.cache.get(roleId);
lines.push(`${emoji || original} : ${role ? role.toString() : `missing role ${roleId}`}`);
}
const message = await interaction.channel.send({ content: lines.join('\n'), allowedMentions: { parse: [] } });
for (const [emoji, original] of emojis) {
try {
if (!emoji) thr
package.json
{
"name": "reaction-roles-js",
"private": true,
"main": "index.js",
"engines": { "node": ">=22" },
"dependencies": { "discord.js": "^14.0.0" }
}