Scan to open on your phone
Point a camera at the QR code - it opens this exact paste, no app needed.
https://codepastes.com/zzgt4399qv09x8mtuntitledPlain Text
import os
import sqlite3
import asyncio
from datetime import datetime, timedelta, timezone
import discord
from discord.ext import commands
from discord import ui
from dotenv import load_dotenv
# ============================================================
# CONFIG
# ============================================================
load_dotenv()
TOKEN = os.getenv("TOKEN")
MOD_LOG_CHANNEL_ID = int(os.getenv("MOD_LOG_CHANNEL_ID", "0"))
WARN_TIMEOUT_AT = int(os.getenv("WARN_TIMEOUT_AT", "3"))
WARN_KICK_AT = int(os.getenv("WARN_KICK_AT", "5"))
WARN_BAN_AT = int(os.getenv("WARN_BAN_AT", "8"))
PREFIX = "."
if not TOKEN:
raise RuntimeError("TOKEN is missing from .env")
# ============================================================
# INTENTS
# ============================================================
intents = discord.Intents.default()
intents.members = True
intents.message_content = True
bot = commands.Bot(
command_prefix=PREFIX,
intents=intents,
help_command=None
)
# ============================================================
# DATABASE
# ============================================================
db = sqlite3.connect(
"moderation.db",
check_same_thread=False
)
db.row_factory = sqlite3.Row
cursor = db.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS warnings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
moderator_id INTEGER NOT NULL,
reason TEXT NOT NULL,
evidence TEXT,
created_at TEXT NOT NULL,
active INTEGER DEFAULT 1
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS appeals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
warning_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
reason TEXT NOT NULL,
created_at TEXT NOT NULL,
status TEXT DEFAULT 'pending'
)
""")
db.commit()
# ============================================================
# DATABASE HELPERS
# ============================================================
def create_warning(
guild_id: int,
user_id: int,
moderator_id: int,
reason: str,
evidence: str | None
):
now = datetime.now(timezone.utc).isoformat()
cursor.execute(
"""
INSERT INTO warnings
(guild_id, user_id, moderator_id, reason, evidence, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
guild_id,
user_id,
moderator_id,
reason,
evidence,
now
)
)
db.commit()
return cursor.lastrowid
def get_warning(warning_id: int):
cursor.execute(
"""
SELECT *
FROM warnings
WHERE id = ?
""",
(warning_id,)
)
return cursor.fetchone()
def get_user_warnings(guild_id: int, user_id: int):
cursor.execute(
"""
SELECT *
FROM warnings
WHERE guild_id = ?
AND user_id = ?
AND active = 1
ORDER BY id DESC
""",
(guild_id, user_id)
)
return cursor.fetchall()
def get_warning_count(guild_id: int, user_id: int):
cursor.execute(
"""
SELECT COUNT(*)
FROM warnings
WHERE guild_id = ?
AND user_id = ?
AND active = 1
""",
(guild_id, user_id)
)
return cursor.fetchone()[0]
def remove_warning(warning_id: int):
cursor.execute(
"""
UPDATE warnings
SET active = 0
WHERE id = ?
""",
(warning_id,)
)
db.commit()
def clear_warnings(guild_id: int, user_id: int):
cursor.execute(
"""
UPDATE warnings
SET active = 0
WHERE guild_id = ?
AND user_id = ?
""",
(guild_id, user_id)
)
db.commit()
def create_appeal(
warning_id: int,
user_id: int,
reason: str
):
now = datetime.now(timezone.utc).isoformat()
cursor.execute(
"""
INSERT INTO appeals
(warning_id, user_id, reason, created_at)
VALUES (?, ?, ?, ?)
""",
(
warning_id,
user_id,
reason,
now
)
)
db.commit()
return cursor.lastrowid
# ============================================================
# UTILS
# ============================================================
def utc_timestamp():
return datetime.now(timezone.utc).strftime(
"%Y-%m-%d %H:%M UTC"
)
def get_log_channel(guild: discord.Guild):
if not MOD_LOG_CHANNEL_ID:
return None
channel = guild.get_channel(MOD_LOG_CHANNEL_ID)
if isinstance(channel, discord.TextChannel):
return channel
return None
def can_moderate(
moderator: discord.Member,
target: discord.Member
):
if moderator.id == target.id:
return False, "You cannot moderate yourself."
if target.id == moderator.guild.owner_id:
return False, "You cannot moderate the server owner."
if moderator.id != moderator.guild.owner_id:
if target.top_role >= moderator.top_role:
return False, "That member has an equal or higher role than you."
if target.top_role >= moderator.guild.me.top_role:
return False, "I cannot moderate that member because of role hierarchy."
return True, None
async def send_log(
guild: discord.Guild,
view: discord.ui.LayoutView
):
channel = get_log_channel(guild)
if not channel:
return
try:
await channel.send(view=view)
except discord.HTTPException:
pass
# ============================================================
# COMPONENTS V2 - WARNING PANEL
# ============================================================
class WarningPanel(discord.ui.LayoutView):
def __init__(
self,
warning_id: int,
member: discord.Member,
moderator: discord.Member,
reason: str,
total_warns: int
):
super().__init__(timeout=None)
self.add_item(
discord.ui.Container(
discord.ui.TextDisplay(
"# ⚠️ Member Warned\n"
f"**User**\n"
f"{member.mention} · `{member.id}`\n\n"
f"**Reason**\n"
f"{reason}\n\n"
f"**Case**\n"
f"`#{warning_id:04d}`\n\n"
f"**Total Warnings**\n"
f"`{total_warns}`\n\n"
f"**Moderator**\n"
f"{moderator.mention}\n\n"
f"-# {utc_timestamp()}"
),
discord.ui.Separator(
visible=True,
spacing=discord.SeparatorSpacing.small
),
discord.ui.ActionRow(
discord.ui.Button(
label="View Warnings",
style=discord.ButtonStyle.secondary,
custom_id=f"warn_view:{warning_id}:{member.id}"
),
discord.ui.Button(
label="Appeal",
style=discord.ButtonStyle.primary,
custom_id=f"warn_appeal:{warning_id}:{member.id}"
)
),
accent_colour=0xFF7A00
)
)
# ============================================================
# COMPONENTS V2 - WARNING LIST
# ============================================================
def build_warning_list(
member: discord.Member,
warnings
):
text = (
f"# Warning History\n"
f"**{member}** · `{member.id}`\n\n"
)
if not warnings:
text += "No active warnings."
else:
for warning in warnings[:15]:
moderator = member.guild.get_member(
warning["moderator_id"]
)
moderator_text = (
moderator.mention
if moderator
else f"`{warning['moderator_id']}`"
)
text += (
f"**Case #{warning['id']:04d}**\n"
f"> {warning['reason']}\n"
f"> Moderator: {moderator_text}\n"
f"> `{warning['created_at']}`\n\n"
)
if len(warnings) > 15:
text += (
f"-# Showing 15 of {len(warnings)} active warnings."
)
return discord.ui.LayoutView(
timeout=120
), text
# ============================================================
# COMPONENTS V2 - APPEAL RESULT
# ============================================================
class AppealPanel(discord.ui.LayoutView):
def __init__(self, appeal_id: int):
super().__init__(timeout=None)
self.add_item(
discord.ui.Container(
discord.ui.TextDisplay(
"# Appeal Submitted\n"
f"Your appeal has been submitted successfully.\n\n"
f"**Appeal ID**\n"
f"`#{appeal_id:04d}`\n\n"
"-# Staff will review your appeal."
),
accent_colour=0x5865F2
)
)
# ============================================================
# WARN COMMAND
# ============================================================
@bot.command(
name="warn",
aliases=["w"]
)
@commands.guild_only()
@commands.has_permissions(moderate_members=True)
async def warn(
ctx: commands.Context,
member: discord.Member,
*,
reason: str = None
):
if not reason:
await ctx.send(
"Usage: `.warn @user <reason>`"
)
return
if len(reason) > 1000:
await ctx.send(
"The reason cannot be longer than 1000 characters."
)
return
if member.bot:
await ctx.send(
"You cannot warn bots."
)
return
moderator = ctx.author
valid, error = can_moderate(
moderator,
member
)
if not valid:
await ctx.send(error)
return
evidence = None
if ctx.message.attachments:
evidence = ctx.message.attachments[0].url
warning_id = create_warning(
guild_id=ctx.guild.id,
user_id=member.id,
moderator_id=moderator.id,
reason=reason,
evidence=evidence
)
total_warns = get_warning_count(
ctx.guild.id,
member.id
)
# ========================================================
# DM USER
# ========================================================
try:
dm_view = discord.ui.LayoutView(
timeout=120
)
dm_view.add_item(
discord.ui.Container(
discord.ui.TextDisplay(
f"# ⚠️ You Have Been Warned\n"
f"**Server**\n"
f"{ctx.guild.name}\n\n"
f"**Reason**\n"
f"{reason}\n\n"
f"**Case**\n"
f"`#{warning_id:04d}`\n\n"
f"**Total Warnings**\n"
f"`{total_warns}`\n\n"
f"-# If you believe this warning was issued incorrectly, "
f"use the appeal option on the warning message."
),
accent_colour=0xFF7A00
)
)
await member.send(
view=dm_view
)
except (
discord.Forbidden,
discord.HTTPException
):
pass
# ========================================================
# MAIN RESPONSE
# ========================================================
panel = WarningPanel(
warning_id,
member,
moderator,
reason,
total_warns
)
await ctx.send(
view=panel
)
# ========================================================
# MOD LOG
# ========================================================
log_view = discord.ui.LayoutView(
timeout=None
)
log_view.add_item(
discord.ui.Container(
discord.ui.TextDisplay(
"# Moderation Log\n"
f"**Action**\n"
`Warning`\n\n"
f"**User**\n"
f"{member.mention} · `{member.id}`\n\n"
f"**Moderator**\n"
f"{moderator.mention}\n\n"
f"**Reason**\n"
f"{reason}\n\n"
f"**Case**\n"
f"`#{warning_id:04d}`\n\n"
f"**Total Warnings**\n"
f"`{total_warns}`"
),
accent_colour=0xFF7A00
)
)
await send_log(
ctx.guild,
log_view
)
# ========================================================
# AUTOMATIC PUNISHMENTS
# ========================================================
try:
if total_warns >= WARN_BAN_AT:
await member.ban(
reason=f"Automatic punishment: {total_warns} warnings"
)
elif total_warns >= WARN_KICK_AT:
await member.kick(
reason=f"Automatic punishment: {total_warns} warnings"
)
elif total_warns >= WARN_TIMEOUT_AT:
await member.timeout(
timedelta(minutes=10),
reason=f"Automatic punishment: {total_warns} warnings"
)
except discord.Forbidden:
pass
except discord.HTTPException:
pass
# ============================================================
# WARNINGS COMMAND
# ============================================================
@bot.command(
name="warnings",
aliases=["warns", "infractions"]
)
@commands.guild_only()
async def warnings(
ctx: commands.Context,
member: discord.Member = None
):
member = member or ctx.author
if (
member != ctx.author
and not ctx.author.guild_permissions.moderate_members
):
await ctx.send(
"You need `Moderate Members` to view another member's warnings."
)
return
data = get_user_warnings(
ctx.guild.id,
member.id
)
view = discord.ui.LayoutView(
timeout=120
)
text = (
f"# Warning History\n"
f"**User:** {member.mention}\n"
f"**Active Warnings:** `{len(data)}`\n\n"
)
if not data:
text += "No active warnings."
else:
for warning in data[:15]:
moderator = ctx.guild.get_member(
warning["moderator_id"]
)
mod_text = (
moderator.mention
if moderator
else f"`{warning['moderator_id']}`"
)
text += (
f"**Case #{warning['id']:04d}**\n"
f"> {warning['reason']}\n"
f"> Moderator: {mod_text}\n"
f"> `{warning['created_at']}`\n\n"
)
view.add_item(
discord.ui.Container(
discord.ui.TextDisplay(text),
accent_colour=0xFF7A00
)
)
await ctx.send(
view=view
)
# ============================================================
# WARN INFO
# ============================================================
@bot.command(
name="warninfo"
)
@commands.guild_only()
@commands.has_permissions(moderate_members=True)
async def warninfo(
ctx: commands.Context,
warning_id: int
):
warning = get_warning(
warning_id
)
if not warning:
await ctx.send(
"That warning case does not exist."
)
return
member = ctx.guild.get_member(
warning["user_id"]
)
moderator = ctx.guild.get_member(
warning["moderator_id"]
)
member_text = (
member.mention
if member
else f"`{warning['user_id']}`"
)
moderator_text = (
moderator.mention
if moderator
else f"`{warning['moderator_id']}`"
)
status = (
"Active"
if warning["active"]
else "Removed"
)
view = discord.ui.LayoutView(
timeout=120
)
view.add_item(
discord.ui.Container(
discord.ui.TextDisplay(
f"# Warning Case #{warning_id:04d}\n\n"
f"**User**\n"
f"{member_text}\n\n"
f"**Moderator**\n"
f"{moderator_text}\n\n"
f"**Reason**\n"
f"{warning['reason']}\n\n"
f"**Status**\n"
f"`{status}`\n\n"
f"**Created**\n"
f"`{warning['created_at']}`\n\n"
f"**Evidence**\n"
f"{warning['evidence'] or 'None'}"
),
accent_colour=0xFF7A00
)
)
await ctx.send(
view=view
)
# ============================================================
# REMOVE WARNING
# ============================================================
@bot.command(
name="unwarn",
aliases=["removewarn"]
)
@commands.guild_only()
@commands.has_permissions(moderate_members=True)
async def unwarn(
ctx: commands.Context,
warning_id: int
):
warning = get_warning(
warning_id
)
if not warning:
await ctx.send(
"That warning case does not exist."
)
return
if not warning["active"]:
await ctx.send(
"That warning has already been removed."
)
return
remove_warning(
warning_id
)
view = discord.ui.LayoutView(
timeout=120
)
view.add_item(
discord.ui.Container(
discord.ui.TextDisplay(
"# Warning Removed\n"
f"Case `#{warning_id:04d}` has been removed.\n\n"
f"**Reason**\n"
f"{warning['reason']}\n\n"
f"**Removed by**\n"
f"{ctx.author.mention}"
),
accent_colour=0x57F287
)
)
await ctx.send(
view=view
)
# ============================================================
# CLEAR WARNINGS
# ============================================================
@bot.command(
name="clearwarns",
aliases=["clearwarnings"]
)
@commands.guild_only()
@commands.has_permissions(administrator=True)
async def clearwarns(
ctx: commands.Context,
member: discord.Member
):
count = get_warning_count(
ctx.guild.id,
member.id
)
if count == 0:
await ctx.send(
f"{member.mention} has no active warnings."
)
return
clear_warnings(
ctx.guild.id,
member.id
)
view = discord.ui.LayoutView(
timeout=120
)
view.add_item(
discord.ui.Container(
discord.ui.TextDisplay(
"# Warnings Cleared\n"
f"All active warnings for {member.mention} "
f"have been removed.\n\n"
f"**Removed**\n"
f"`{count}` warnings\n\n"
f"**Moderator**\n"
f"{ctx.author.mention}"
),
accent_colour=0x57F287
)
)
await ctx.send(
view=view
)
# ============================================================
# WARN HELP
# ============================================================
@bot.command(
name="warnhelp"
)
async def warnhelp(
ctx: commands.Context
):
view = discord.ui.LayoutView(
timeout=120
)
view.add_item(
discord.ui.Container(
discord.ui.TextDisplay(
"# Moderation Commands\n\n"
"**`.warn @user <reason>`**\n"
"Issue a warning.\n\n"
"**`.warnings [@user]`**\n"
"View active warnings.\n\n"
"**`.warninfo <case>`**\n"
"View a specific warning case.\n\n"
"**`.unwarn <case>`**\n"
"Remove a warning.\n\n"
"**`.clearwarns @user`**\n"
"Clear all active warnings.\n\n"
"-# Staff permissions are required for moderation commands."
),
accent_colour=0x5865F2
)
)
await ctx.send(
view=view
)
# ============================================================
# COMPONENT INTERACTIONS
# ============================================================
@bot.event
async def on_interaction(
interaction: discord.Interaction
):
if not interaction.data:
return
custom_id = interaction.data.get(
"custom_id"
)
if not custom_id:
return
# ========================================================
# VIEW WARNINGS
# ========================================================
if custom_id.startswith("warn_view:"):
parts = custom_id.split(":")
warning_id = int(parts[1])
user_id = int(parts[2])
if not interaction.guild:
return
member = interaction.guild.get_member(
user_id
)
if not member:
return await interaction.response.send_message(
"That member is no longer in the server.",
ephemeral=True
)
if (
interaction.user.id != user_id
and not isinstance(
interaction.user,
discord.Member
)
):
return await interaction.response.send_message(
"You cannot view these warnings.",
ephemeral=True
)
if (
interaction.user.id != user_id
and not interaction.user.guild_permissions.moderate_members
):
return await interaction.response.send_message(
"You need `Moderate Members` to view another member's warnings.",
ephemeral=True
)
data = get_user_warnings(
interaction.guild.id,
user_id
)
view = discord.ui.LayoutView(
timeout=120
)
text = (
f"# Warning History\n"
f"**User:** {member.mention}\n"
f"**Active:** `{len(data)}`\n\n"
)
if not data:
text += "No active warnings."
else:
for warning in data[:15]:
moderator = interaction.guild.get_member(
warning["moderator_id"]
)
moderator_text = (
moderator.mention
if moderator
else f"`{warning['moderator_id']}`"
)
text += (
f"**Case #{warning['id']:04d}**\n"
f"> {warning['reason']}\n"
f"> Moderator: {moderator_text}\n"
f"> `{warning['created_at']}`\n\n"
)
view.add_item(
discord.ui.Container(
discord.ui.TextDisplay(text),
accent_colour=0xFF7A00
)
)
await interaction.response.send_message(
view=view,
ephemeral=True
)
return
# ========================================================
# APPEAL
# ========================================================
if custom_id.startswith("warn_appeal:"):
parts = custom_id.split(":")
warning_id = int(parts[1])
user_id = int(parts[2])
if interaction.user.id != user_id:
return await interaction.response.send_message(
"Only the warned member can appeal this warning.",
ephemeral=True
)
warning = get_warning(
warning_id
)
if not warning:
return await interaction.response.send_message(
"That warning no longer exists.",
ephemeral=True
)
modal = AppealModal(
warning_id=warning_id
)
await interaction.response.send_modal(
modal
)
return
# ============================================================
# APPEAL MODAL
# ============================================================
class AppealModal(discord.ui.Modal):
def __init__(self, warning_id: int):
super().__init__(
title=f"Appeal Warning #{warning_id:04d}"
)
self.warning_id = warning_id
self.reason = discord.ui.TextInput(
label="Why should this warning be removed?",
placeholder="Explain your appeal...",
style=discord.TextStyle.paragraph,
min_length=10,
max_length=1000,
required=True
)
self.add_item(
self.reason
)
async def on_submit(
self,
interaction: discord.Interaction
):
appeal_id = create_appeal(
warning_id=self.warning_id,
user_id=interaction.user.id,
reason=str(self.reason.value)
)
# ----------------------------------------------------
# SEND APPEAL TO MOD LOG
# ----------------------------------------------------
if interaction.guild:
log_view = discord.ui.LayoutView(
timeout=None
)
log_view.add_item(
discord.ui.Container(
discord.ui.TextDisplay(
"# New Warning Appeal\n"
f"**Appeal**\n"
f"`#{appeal_id:04d}`\n\n"
f"**Warning**\n"
f"`#{self.warning_id:04d}`\n\n"
f"**User**\n"
f"{interaction.user.mention}\n\n"
f"**Reason**\n"
f"{self.reason.value}\n\n"
f"-# Status: Pending"
),
accent_colour=0x5865F2
)
)
await send_log(
interaction.guild,
log_view
)
# ----------------------------------------------------
# USER RESPONSE
# ----------------------------------------------------
await interaction.response.send_message(
view=AppealPanel(
appeal_id
),
ephemeral=True
)
# ============================================================
# COMMAND ERROR HANDLER
# ============================================================
@bot.event
async def on_command_error(
ctx: commands.Context,
error: commands.CommandError
):
if isinstance(
error,
commands.CommandNotFound
):
return
if isinstance(
error,
commands.MissingPermissions
):
await ctx.send(
"You do not have permission to use that command."
)
return
if isinstance(
error,
commands.MissingRequiredArgument
):
await ctx.send(
f"Missing argument.\n"
f"Use `.warnhelp` for command usage."
)
return
if isinstance(
error,
commands.BadArgument
):
await ctx.send(
"I couldn't find that member or case."
)
return
print(
f"[COMMAND ERROR] {type(error).__name__}: {error}"
)
# ============================================================
# READY
# ============================================================
@bot.event
async def on_ready():
print("----------------------------------------")
print(f"Logged in as: {bot.user}")
print(f"Bot ID: {bot.user.id}")
print(f"Discord.py: {discord.__version__}")
print("----------------------------------------")
print("Moderation system loaded.")
print(f"Prefix: {PREFIX}")
print(f"Timeout threshold: {WARN_TIMEOUT_AT}")
print(f"Kick threshold: {WARN_KICK_AT}")
print(f"Ban threshold: {WARN_BAN_AT}")
print("----------------------------------------")
# ============================================================
# START
# ============================================================
bot.run(TOKEN)28,375 chars · codepastes.com