Moderation bot (JavaScript)
Kick, ban and timeout slash commands that check both your roles and the bot's before acting.
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. |
MOD_LOG_CHANNEL_ID | Optional. Channel where every action is logged |
Files
README.md
# Moderation bot
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
- `/kick member [reason]`
- `/ban member [reason] [delete_days 0-7]`
- `/timeout member minutes [reason]` (up to 28 days)
- Each command checks that you have the permission, that the target is not you, the server owner or the bot, and that your highest role and the bot's highest role are both above the target's. Server owners skip the check on their own roles.
- Server admins can change who sees the commands under Server Settings, Integrations.
## 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: Kick Members, Ban Members, Moderate Members, Send Messages.
3. Set these values on the Secrets tab. The ones marked optional can stay empty:
- `MOD_LOG_CHANNEL_ID`: Optional. Channel where every action is logged.
4. Press Deploy. The console shows a line with "Logged in as" once the bot is online.
index.js
const {
Client, Events, GatewayIntentBits, MessageFlags, PermissionFlagsBits, SlashCommandBuilder,
} = require('discord.js');
const LOG_ID = (process.env.MOD_LOG_CHANNEL_ID || '').trim();
const MOD_LOG_CHANNEL_ID = /^\d+$/.test(LOG_ID) ? LOG_ID : null;
const MAX_TIMEOUT_MINUTES = 28 * 24 * 60; // Discord's limit is 28 days
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const withTarget = (builder, verb) => builder
.addUserOption((o) => o.setName('member').setDescription(`Member to ${verb}`).setRequired(true));
const withReason = (builder) => builder
.addStringOption((o) => o.setName('reason').setDescription('Reason for the audit log'));
const commands = [
withReason(withTarget(new SlashCommandBuilder().setName('kick').setDescription('Kick a member')
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers), 'kick')),
withReason(withTarget(new SlashCommandBuilder().setName('ban').setDescription('Ban a member')
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers), 'ban'))
.addIntegerOption((o) => o.setName('delete_days')
.setDescription('Days of their messages to delete (0 to 7)').setMinValue(0).setMaxValue(7)),
withReason(withTarget(new SlashCommandBuilder().setName('timeout').setDescription('Time out a member')
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), 'time out')
.addIntegerOption((o) => o.setName('minutes').setDescription('Length in minutes (max 28 days)')
.setRequired(true).setMinValue(1).setMaxValue(MAX_TIMEOUT_MINUTES))),
].map((c) => c.toJSON());
const PERMS = {
kick: [PermissionFlagsBits.KickMembers, 'Kick Members'],
ban: [PermissionFlagsBits.BanMembers, 'Ban Members'],
timeout: [PermissionFlagsBits.ModerateMembers, 'Timeout Members'],
};
// Returns an error message, or null when the action is allowed.
function checkTarget(interaction, target, perm, label) {
const { guild, member: actor } = interaction;
const me = guild.members.me;
if (!interaction.memberPermissions.has(perm)) return `You need the ${label} permission to do that.`;
if (!me.permissions.has(perm)) return `I need the ${label} permission to do that.`;
if (!target) return 'That user is not a member of this server.';
if (target.id === actor.id) return 'You cannot use this on yourself.';
if (target.id === guild.ownerId) return 'You cannot use this on the server owner.';
if (target.id === me.id) return 'I cannot use this on myself.';
if (actor.id !== guild.ownerId && actor.roles.highest.comparePositionTo(target.roles.highest) <= 0) {
return "Your highest role must be above the target's highest role.";
}
if (me.roles.highest.comparePositionTo(target.roles.highest) <= 0) {
return "My highest role must be above the target's highest role.";
}
return null;
}
async function modLog(guild, text) {
if (!MOD_LOG_CHANNEL_ID) return;
const channel = guild.channels.cache.get(MOD_LOG_CHANNEL_ID);
if (!channel || !channel.isTextBased()) {
console.log(`MOD_LOG_CHANNEL_ID ${MOD_LOG_CHANNEL_ID} not found`);
return;
}
await channel.send({ content: text, allowedMentions: { parse: [] } })
.catch((err) => console.error('Could not write to the mod log:', err.message));
}
async function handle(interaction) {
const name = interaction.commandName;
if (!PERMS[name]) return;
const [perm, label] = PERMS[name];
const target = interaction.options.getMember('member');
const reason = interaction.options.getString('reason');
const error = checkTarget(interaction, target, perm, label);
if (error) {
await interaction.reply({ content: error, flags: MessageFlags.Ephemeral });
return;
}
const auditReason = `${interaction.user.tag} (${interaction.user.id}): ${reason || 'no reason given'}`;
let verb;
if (name === 'kick') {
await target.kick(auditReason);
verb = 'kicked';
} else if (name === 'ban') {
const days = interaction.options.getInteger('delete_days') ?? 0;
await interaction.guild
package.json
{
"name": "moderation-bot-js",
"private": true,
"main": "index.js",
"engines": { "node": ">=22" },
"dependencies": { "discord.js": "^14.0.0" }
}