Moderation bot (Python)
Kick, ban and timeout slash commands that check both your roles and the bot's before acting.
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. |
MOD_LOG_CHANNEL_ID | Optional. Channel where every action is logged |
Files
README.md
# Moderation bot
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
- `/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.
main.py
import datetime
import os
import discord
from discord import app_commands
LOG_VALUE = os.environ.get("MOD_LOG_CHANNEL_ID", "").strip()
MOD_LOG_CHANNEL_ID = int(LOG_VALUE) if LOG_VALUE.isdigit() else None
MAX_TIMEOUT_MINUTES = 28 * 24 * 60 # Discord's limit is 28 days
class ModBot(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 = ModBot()
def check_target(interaction, target, perm, perm_label):
"""Return an error message, or None when the action is allowed."""
guild = interaction.guild
actor = interaction.user
me = guild.me
if not getattr(actor.guild_permissions, perm):
return f"You need the {perm_label} permission to do that."
if not getattr(me.guild_permissions, perm):
return f"I need the {perm_label} permission to do that."
if target.id == actor.id:
return "You cannot use this on yourself."
if target.id == guild.owner_id:
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.owner_id and actor.top_role <= target.top_role:
return "Your highest role must be above the target's highest role."
if me.top_role <= target.top_role:
return "My highest role must be above the target's highest role."
return None
async def mod_log(guild, text):
if MOD_LOG_CHANNEL_ID is None:
return
channel = guild.get_channel(MOD_LOG_CHANNEL_ID)
if channel is None:
print(f"MOD_LOG_CHANNEL_ID {MOD_LOG_CHANNEL_ID} not found")
return
try:
await channel.send(text, allowed_mentions=discord.AllowedMentions.none())
except discord.HTTPException as exc:
print(f"Could not write to the mod log: {exc}")
async def run_action(interaction, target, perm, perm_label, verb, action, reason):
error = check_target(interaction, target, perm, perm_label)
if error:
await interaction.response.send_message(error, ephemeral=True)
return
audit_reason = f"{interaction.user} ({interaction.user.id}): {reason or 'no reason given'}"
try:
await action(audit_reason)
except discord.Forbidden:
await interaction.response.send_message(
"Discord refused the action. Check my role position and permissions.",
ephemeral=True)
return
summary = f"{target} ({target.id}) was {verb} by {interaction.user.mention}."
if reason:
summary += f" Reason: {reason}"
await interaction.response.send_message(summary, ephemeral=True)
await mod_log(interaction.guild, summary)
@bot.tree.command(name="kick", description="Kick a member")
@app_commands.describe(member="Member to kick", reason="Reason for the audit log")
@app_commands.default_permissions(kick_members=True)
@app_commands.guild_only()
async def kick(interaction: discord.Interaction, member: discord.Member,
reason: str | None = None):
await run_action(interaction, member, "kick_members", "Kick Members", "kicked",
lambda r: member.kick(reason=r), reason)
@bot.tree.command(name="ban", description="Ban a member")
@app_commands.describe(member="Member to ban", reason="Reason for the audit log",
delete_days="Days of their messages to delete (0 to 7)")
@app_commands.default_permissions(ban_members=True)
@app_commands.guild_only()
async def ban(interaction: discord.Interaction, member: discord.Member,
reason: str | None = None,
delete_days: app_commands.Range[int, 0, 7] = 0):
await run_action(interaction, member, "ban_members", "Ban Members", "banned",
lambda r: interaction.guild.ban(
member, reason=r, delete_message_seconds=delete_days * 86400),
reason)
@bot.tree.command(name
requirements.txt
discord.py