From 508399fe1fb15c3c55107b0286985734c5c8ec95 Mon Sep 17 00:00:00 2001 From: BaileyCodes Date: Tue, 14 Apr 2026 16:10:36 -0500 Subject: [PATCH] Punisher bot source --- punisher.py | 510 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 510 insertions(+) create mode 100644 punisher.py diff --git a/punisher.py b/punisher.py new file mode 100644 index 0000000..36c696a --- /dev/null +++ b/punisher.py @@ -0,0 +1,510 @@ +import discord +from discord import app_commands +from discord.ext import commands +import sqlite3 +import os +import logging +import datetime +from logging.handlers import RotatingFileHandler +from dotenv import load_dotenv + +load_dotenv() + +TOKEN = os.getenv("DISCORD_TOKEN") +DB_PATH = os.getenv("DB_PATH", "data/blacklist.db") +AUTH_USERS_FILE = os.getenv("AUTH_USERS_FILE", "data/authorized_users.txt") +LOG_GUILD_ID = 1491646567097307136 +LOG_CHANNEL_ID = 1491648117483770037 +APPEALS_GUILD_ID = 1491644365020467210 + +# ─── Logging ────────────────────────────────────────────────────────────────── + +os.makedirs("logs", exist_ok=True) +os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + handlers=[ + RotatingFileHandler("logs/bot.log", maxBytes=5_000_000, backupCount=3), + logging.StreamHandler(), + ], +) +log = logging.getLogger("eagler") + +# ─── Intents ────────────────────────────────────────────────────────────────── + +intents = discord.Intents.default() +intents.members = True + +bot = commands.Bot(command_prefix="!", intents=intents) +tree = bot.tree + +# ─── Authorized users ───────────────────────────────────────────────────────── + +def load_authorized_users() -> set[str]: + """Read authorized_users.txt and return a set of user ID strings.""" + if not os.path.exists(AUTH_USERS_FILE): + return set() + with open(AUTH_USERS_FILE) as f: + ids = set() + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + # Strip inline comments (e.g. "123456 # username") + user_id = line.split("#")[0].strip() + if user_id: + ids.add(user_id) + return ids + +def is_authorized(user_id: int) -> bool: + return str(user_id) in load_authorized_users() + +# ─── Database ───────────────────────────────────────────────────────────────── + +def get_conn() -> sqlite3.Connection: + return sqlite3.connect(DB_PATH) + +def init_db(): + with get_conn() as conn: + conn.execute(""" + CREATE TABLE IF NOT EXISTS blacklist ( + user_id TEXT PRIMARY KEY, + reason TEXT, + banned_by TEXT, + banned_at TEXT + ) + """) + conn.execute(""" + CREATE TABLE IF NOT EXISTS pardons ( + user_id TEXT, + guild_id TEXT, + PRIMARY KEY (user_id, guild_id) + ) + """) + conn.commit() + +def bl_add(user_id: str, reason: str, banned_by: str): + with get_conn() as conn: + conn.execute( + "INSERT OR REPLACE INTO blacklist VALUES (?,?,?,?)", + (user_id, reason, banned_by, datetime.datetime.utcnow().isoformat()) + ) + conn.commit() + +def bl_remove(user_id: str) -> bool: + with get_conn() as conn: + cur = conn.execute("DELETE FROM blacklist WHERE user_id=?", (user_id,)) + conn.commit() + return cur.rowcount > 0 + +def bl_get(user_id: str): + with get_conn() as conn: + return conn.execute( + "SELECT reason, banned_by, banned_at FROM blacklist WHERE user_id=?", + (user_id,) + ).fetchone() + +def bl_all(): + with get_conn() as conn: + return conn.execute( + "SELECT user_id, reason, banned_by, banned_at FROM blacklist ORDER BY banned_at DESC" + ).fetchall() + +def pardon_add(user_id: str, guild_id: str): + with get_conn() as conn: + conn.execute("INSERT OR IGNORE INTO pardons VALUES (?,?)", (user_id, guild_id)) + conn.commit() + +def pardon_exists(user_id: str, guild_id: str) -> bool: + with get_conn() as conn: + return conn.execute( + "SELECT 1 FROM pardons WHERE user_id=? AND guild_id=?", (user_id, guild_id) + ).fetchone() is not None + +# ─── Permission checks ──────────────────────────────────────────────────────── + +def requires_admin(): + """All commands: must have Administrator in the server.""" + async def predicate(interaction: discord.Interaction) -> bool: + if not interaction.user.guild_permissions.administrator: + await interaction.response.send_message( + ":no_entry: You need the **Administrator** permission to use this command.", + ephemeral=True + ) + return False + return True + return app_commands.check(predicate) + +def requires_authorized(): + """ban/unban: must be Administrator AND in authorized_users.txt.""" + async def predicate(interaction: discord.Interaction) -> bool: + if not interaction.user.guild_permissions.administrator: + await interaction.response.send_message( + ":no_entry: You need the **Administrator** permission to use this command.", + ephemeral=True + ) + return False + if not is_authorized(interaction.user.id): + await interaction.response.send_message( + ":no_entry: You are not in the authorized users list for this command.", + ephemeral=True + ) + return False + return True + return app_commands.check(predicate) + +def parse_user_id(raw: str) -> int | None: + try: + return int(raw.strip()) + except ValueError: + return None + +async def send_log(actioned_by: discord.User | discord.Member, target_user, target_id: str, reason: str, server_count: int | None, action: str): + guild = bot.get_guild(LOG_GUILD_ID) + if not guild: + log.warning("Ban log guild not found (%s)", LOG_GUILD_ID) + return + channel = guild.get_channel(LOG_CHANNEL_ID) + if not channel: + log.warning("Ban log channel not found (%s)", LOG_CHANNEL_ID) + return + + target_display = f"{target_user.mention}\n`{target_id}`" if target_user else f"<@{target_id}>\n`{target_id}`" + is_ban = action == "ban" + + embed = discord.Embed( + title=f"Eagler Enforcement — {'Ban' if is_ban else 'Unban'} Log", + color=discord.Color.red() if is_ban else discord.Color.green(), + timestamp=datetime.datetime.utcnow() + ) + embed.add_field(name="Actioned By", value=f"{actioned_by.mention}\n`{actioned_by.id}`", inline=True) + embed.add_field(name=f"{'Banned' if is_ban else 'Unbanned'} User", value=target_display, inline=True) + embed.add_field(name="Reason", value=reason, inline=False) + if server_count is not None: + embed.add_field(name="Servers Banned From", value=str(server_count), inline=True) + embed.add_field(name="Date", value=f"", inline=True) + embed.set_footer(text="Eagler Enforcement") + + try: + await channel.send(embed=embed) + except discord.Forbidden: + log.warning("Missing permission to send to ban log channel") + +# ─── Events ─────────────────────────────────────────────────────────────────── + +@bot.event +async def on_ready(): + init_db() + # Create the authorized_users.txt if it doesn't exist yet + if not os.path.exists(AUTH_USERS_FILE): + with open(AUTH_USERS_FILE, "w") as f: + f.write("# Eagler Enforcement — Authorized Users\n") + f.write("# Add one Discord user ID per line.\n") + f.write("# These users are allowed to use /ban and /unban.\n") + f.write("# Lines starting with # are ignored.\n") + synced = await tree.sync() + log.info("Logged in as %s | Synced %d command(s)", bot.user, len(synced)) + +@bot.event +async def on_guild_join(guild: discord.Guild): + owner = guild.owner or await bot.fetch_user(guild.owner_id) + if not owner: + return + try: + embed = discord.Embed( + title="Thanks for using Eagler Enforcement!", + color=discord.Color.dark_red() + ) + embed.add_field( + name="**(VERY RECOMMENDED)** Join the Server Owner Guild", + value="https://discord.gg/BEHrUkdHzs", + inline=False + ) + embed.add_field( + name="\u200b", + value="Have fun keeping your server safe :)", + inline=False + ) + embed.set_footer(text="Eagler Enforcement") + await owner.send(embed=embed) + except discord.Forbidden: + pass + +@bot.event +async def on_member_join(member: discord.Member): + if member.guild.id == APPEALS_GUILD_ID: + return + row = bl_get(str(member.id)) + if not row: + return + if pardon_exists(str(member.id), str(member.guild.id)): + return + + reason, banned_by, _ = row + try: + dm_embed = discord.Embed( + title="You have been blacklisted from Eagler Enforcement", + color=discord.Color.red(), + timestamp=datetime.datetime.utcnow() + ) + dm_embed.add_field(name="Reason", value=reason, inline=False) + dm_embed.add_field(name="Banned By", value=f"<@{banned_by}> (`{banned_by}`)", inline=True) + dm_embed.add_field(name="Appeal", value="[Click here to join the appeals server](https://discord.gg/PMJ99n7yye)", inline=False) + dm_embed.set_footer(text="Eagler Enforcement") + await member.send(embed=dm_embed) + except discord.Forbidden: + pass + try: + await member.guild.ban( + member, + reason=f"Eagler Enforcement Blacklist: {reason}", + delete_message_days=0 + ) + except discord.Forbidden: + pass + +# ─── /ban ───────────────────────────────────────────────────────────────────── + +@tree.command(name="ban", description="Blacklist a player from Eagler Enforcement") +@app_commands.describe(userid="The user ID to blacklist", reason="Reason for the blacklist") +@requires_authorized() +async def ban_cmd(interaction: discord.Interaction, userid: str, reason: str): + await interaction.response.defer(ephemeral=True) + + uid = parse_user_id(userid) + if uid is None: + await interaction.followup.send("Invalid user ID — must be a numeric snowflake.", ephemeral=True) + return + + if bl_get(userid): + await interaction.followup.send(f"User `{userid}` is already blacklisted.", ephemeral=True) + return + + bl_add(userid, reason, str(interaction.user.id)) + + # Fetch target user object early (needed for DM and log) + try: + target = bot.get_user(uid) or await bot.fetch_user(uid) + except discord.NotFound: + target = None + + # Ban from every guild the bot is in (except the appeals server) + banned_from, failed = [], [] + for guild in bot.guilds: + if guild.id == APPEALS_GUILD_ID: + continue + if pardon_exists(userid, str(guild.id)): + continue + try: + await guild.ban( + discord.Object(id=uid), + reason=f"Eagler Enforcement Blacklist: {reason}", + delete_message_days=0 + ) + banned_from.append(guild.name) + except discord.Forbidden: + failed.append(guild.name) + except Exception: + failed.append(guild.name) + + # DM the user now that we know the server count + if target: + try: + dm_embed = discord.Embed( + title="You have been blacklisted from Eagler Enforcement", + color=discord.Color.red(), + timestamp=datetime.datetime.utcnow() + ) + dm_embed.add_field(name="Reason", value=reason, inline=False) + dm_embed.add_field(name="Banned By", value=f"{interaction.user} (`{interaction.user.id}`)", inline=True) + dm_embed.add_field(name="Servers Banned From", value=str(len(banned_from)), inline=True) + dm_embed.add_field(name="Appeal", value="[Click here to join the appeals server](https://discord.gg/PMJ99n7yye)", inline=False) + dm_embed.set_footer(text="Eagler Enforcement") + await target.send(embed=dm_embed) + except discord.Forbidden: + pass + + await send_log(interaction.user, target, userid, reason, len(banned_from), "ban") + + username = str(target) if target else "Unknown User" + embed = discord.Embed( + title="User Blacklisted", + color=discord.Color.red(), + timestamp=datetime.datetime.utcnow() + ) + embed.add_field(name="User", value=f"{username}\n`{userid}`", inline=True) + embed.add_field(name="Reason", value=reason, inline=True) + embed.add_field(name="Servers Banned", value=str(len(banned_from)), inline=True) + if failed: + embed.add_field( + name="Failed — Missing Permission", + value="\n".join(failed) or "None", + inline=False + ) + embed.set_footer(text=f"Actioned by {interaction.user}") + await interaction.followup.send(embed=embed, ephemeral=True) + +# ─── /unban ─────────────────────────────────────────────────────────────────── + +@tree.command(name="unban", description="Remove the blacklist from a player within Eagler Enforcement") +@app_commands.describe(userid="The user ID to unblacklist", reason="Reason for the removal") +@requires_authorized() +async def unban_cmd(interaction: discord.Interaction, userid: str, reason: str): + await interaction.response.defer(ephemeral=True) + + uid = parse_user_id(userid) + if uid is None: + await interaction.followup.send("Invalid user ID — must be a numeric snowflake.", ephemeral=True) + return + + if not bl_remove(userid): + await interaction.followup.send(f"User `{userid}` is not blacklisted.", ephemeral=True) + return + + # Lift the ban from every guild the bot is in + unbanned_from, failed = [], [] + for guild in bot.guilds: + if guild.id == APPEALS_GUILD_ID: + continue + try: + await guild.unban( + discord.Object(id=uid), + reason=f"Eagler Enforcement unban: {reason}" + ) + unbanned_from.append(guild.name) + except discord.NotFound: + pass # wasn't banned there, skip + except discord.Forbidden: + failed.append(guild.name) + + try: + target = bot.get_user(uid) or await bot.fetch_user(uid) + username = str(target) if target else "Unknown User" + except discord.NotFound: + target = None + username = "Unknown User" + + await send_log(interaction.user, target, userid, reason, None, "unban") + + embed = discord.Embed( + title="User Unblacklisted", + color=discord.Color.green(), + timestamp=datetime.datetime.utcnow() + ) + embed.add_field(name="User", value=f"{username}\n`{userid}`", inline=True) + embed.add_field(name="Reason", value=reason, inline=True) + embed.add_field(name="Servers Unbanned From", value=str(len(unbanned_from)), inline=True) + if failed: + embed.add_field(name="Failed — Missing Permission", value="\n".join(failed), inline=False) + embed.set_footer(text=f"Actioned by {interaction.user}") + await interaction.followup.send(embed=embed, ephemeral=True) + +# ─── /pardon ────────────────────────────────────────────────────────────────── + +@tree.command(name="pardon", description="Pardon a player's ban from this specific server, not all servers") +@app_commands.describe(userid="The user ID to pardon in this server") +@requires_admin() +async def pardon_cmd(interaction: discord.Interaction, userid: str): + await interaction.response.defer(ephemeral=True) + + uid = parse_user_id(userid) + if uid is None: + await interaction.followup.send("Invalid user ID — must be a numeric snowflake.", ephemeral=True) + return + + try: + await interaction.guild.unban( + discord.Object(id=uid), + reason=f"Eagler Enforcement pardon issued by {interaction.user}" + ) + except discord.NotFound: + pass + except discord.Forbidden: + await interaction.followup.send( + "I don't have permission to unban members in this server.", ephemeral=True + ) + return + + pardon_add(userid, str(interaction.guild.id)) + + still_global = bl_get(userid) is not None + note = ( + "This user is still on the **global blacklist** — they will not be re-banned when joining " + "**this server**, but remain banned everywhere else." + if still_global + else "Note: this user is not currently on the global blacklist." + ) + + try: + target = bot.get_user(uid) or await bot.fetch_user(uid) + username = str(target) if target else "Unknown User" + except discord.NotFound: + username = "Unknown User" + + embed = discord.Embed( + title="User Pardoned (This Server Only)", + color=discord.Color.blue(), + timestamp=datetime.datetime.utcnow() + ) + embed.add_field(name="User", value=f"{username}\n`{userid}`", inline=True) + embed.add_field(name="Server", value=interaction.guild.name, inline=True) + embed.add_field(name="Note", value=note, inline=False) + embed.set_footer(text=f"Actioned by {interaction.user}") + await interaction.followup.send(embed=embed, ephemeral=True) + +# ─── /list ──────────────────────────────────────────────────────────────────── + +@tree.command(name="list", description="Shows a list of all blacklisted players") +@requires_admin() +async def list_cmd(interaction: discord.Interaction): + await interaction.response.defer(ephemeral=True) + + rows = bl_all() + if not rows: + await interaction.followup.send("No users are currently blacklisted.", ephemeral=True) + return + + embeds: list[discord.Embed] = [] + current = discord.Embed( + title="Eagler Enforcement — Blacklist", + color=discord.Color.dark_red(), + timestamp=datetime.datetime.utcnow() + ) + current.set_footer(text=f"{len(rows)} blacklisted user(s)") + field_count = 0 + + for user_id, reason, banned_by, banned_at in rows: + try: + user = bot.get_user(int(user_id)) or await bot.fetch_user(int(user_id)) + username = str(user) + except Exception: + username = "Unknown User" + + date_str = banned_at[:10] if banned_at else "Unknown" + value = f"`{user_id}`\nReason: {reason}\nDate: {date_str}" + + if field_count >= 24: + embeds.append(current) + current = discord.Embed( + title="Eagler Enforcement — Blacklist (cont.)", + color=discord.Color.dark_red() + ) + field_count = 0 + + current.add_field(name=username, value=value, inline=True) + field_count += 1 + + embeds.append(current) + + await interaction.followup.send(embed=embeds[0], ephemeral=True) + for embed in embeds[1:]: + await interaction.followup.send(embed=embed, ephemeral=True) + +# ─── Run ────────────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + if not TOKEN: + raise RuntimeError("DISCORD_TOKEN is not set. Copy .env.example to .env and fill it in.") + bot.run(TOKEN)