From d243293ad9cf59a192da331db4332bf348c954f7 Mon Sep 17 00:00:00 2001 From: Lilac-Rose Date: Wed, 8 Oct 2025 22:08:24 +0200 Subject: [PATCH] Updated rank.py to make the message estimator take multiplier into account --- moderation/loader.py | 148 +++++++++++++++++++++++++++++++++++++++++- moderation/mute.py | 65 ++++++++++++------- moderation/unmmute.py | 20 ++++-- xp/rank.py | 4 +- 4 files changed, 204 insertions(+), 33 deletions(-) diff --git a/moderation/loader.py b/moderation/loader.py index a12e826..1acbc3f 100644 --- a/moderation/loader.py +++ b/moderation/loader.py @@ -1,7 +1,8 @@ import discord -from discord.ext import commands +from discord.ext import commands, tasks import os import sqlite3 +import asyncio from dotenv import load_dotenv from datetime import datetime @@ -18,12 +19,16 @@ class ModerationBase(commands.Cog): self.conn.row_factory = sqlite3.Row self.c = self.conn.cursor() self.initialize_db() + self.check_mutes.start() + print(f"βœ… ModerationBase initialized. DB path: {self.db_path}") def cog_unload(self): """Ensure database connection closes when the cog unloads.""" + self.check_mutes.cancel() self.conn.close() def initialize_db(self): + # Infractions table self.c.execute(""" CREATE TABLE IF NOT EXISTS infractions ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -35,6 +40,16 @@ class ModerationBase(commands.Cog): timestamp TEXT NOT NULL ) """) + # Mutes table + self.c.execute(""" + CREATE TABLE IF NOT EXISTS mutes ( + user_id INTEGER NOT NULL, + guild_id INTEGER NOT NULL, + unmute_time TEXT NOT NULL, + channel_id INTEGER NOT NULL, + PRIMARY KEY (user_id, guild_id) + ) + """) self.conn.commit() @staticmethod @@ -55,5 +70,134 @@ class ModerationBase(commands.Cog): """, (user_id, guild_id, type_, reason, mod_id, datetime.utcnow().isoformat())) self.conn.commit() + @tasks.loop(seconds=30) + async def check_mutes(self): + """Periodically check for expired mutes.""" + if not self.bot.is_ready(): + print("⏳ Bot not ready yet, skipping mute check") + return + + now = datetime.utcnow() + + print(f"\nπŸ” Checking mutes at {now.isoformat()}") + print(f" Bot is in {len(self.bot.guilds)} guild(s)") + + self.c.execute("SELECT * FROM mutes") + mutes = self.c.fetchall() + + if not mutes: + print(" No active mutes in database") + return + + print(f" Found {len(mutes)} active mute(s)") + + for mute in mutes: + print(f"\n Checking mute for user {mute['user_id']} in guild {mute['guild_id']}") + + try: + unmute_time = datetime.fromisoformat(mute["unmute_time"]) + print(f" Unmute time: {unmute_time.isoformat()}") + print(f" Current time: {now.isoformat()}") + print(f" Should unmute: {now >= unmute_time}") + except ValueError as e: + print(f" ❌ Error parsing unmute time: {e}") + continue + + if now >= unmute_time: + print(f" ⏰ Time to unmute user {mute['user_id']}") + + # List all guilds bot can see + print(f" Bot can see guilds: {[g.id for g in self.bot.guilds]}") + + guild = self.bot.get_guild(mute["guild_id"]) + if not guild: + print(f" ❌ Guild {mute['guild_id']} not found in bot's guild cache") + print(f" This likely means the bot left the server or has incorrect intents") + # Remove stale mute + self.c.execute("DELETE FROM mutes WHERE user_id = ? AND guild_id = ?", + (mute["user_id"], mute["guild_id"])) + self.conn.commit() + continue + + print(f" βœ… Found guild: {guild.name}") + + # Fetch member, even if offline + try: + member = guild.get_member(mute["user_id"]) + if not member: + print(f" Member not in cache, fetching...") + member = await guild.fetch_member(mute["user_id"]) + print(f" βœ… Fetched member {member}") + else: + print(f" βœ… Found member {member}") + except discord.NotFound: + print(f" ❌ Member {mute['user_id']} not found in guild (may have left)") + # Remove from DB since user left + self.c.execute("DELETE FROM mutes WHERE user_id = ? AND guild_id = ?", + (mute["user_id"], mute["guild_id"])) + self.conn.commit() + continue + except Exception as e: + print(f" ❌ Error fetching member: {e}") + continue + + mute_role = guild.get_role(982702037517090836) + if not mute_role: + print(f" ❌ Mute role (982702037517090836) not found in guild") + continue + + print(f" βœ… Found mute role: {mute_role.name}") + print(f" Member has mute role: {mute_role in member.roles}") + print(f" Member's roles: {[r.name for r in member.roles]}") + + if mute_role in member.roles: + print(f" Attempting to remove mute role...") + try: + await member.remove_roles(mute_role, reason="Mute duration expired") + print(f" βœ… Successfully removed mute role") + + await self.log_infraction( + guild.id, member.id, self.bot.user.id, + "unmute", "Automatic unmute (duration expired)" + ) + print(f" βœ… Logged infraction") + + # Send message in the original channel + channel = guild.get_channel(mute["channel_id"]) + if channel: + try: + await channel.send(f"{member.mention} has been automatically unmuted (duration expired).") + print(f" βœ… Sent unmute message in #{channel.name}") + except Exception as e: + print(f" ⚠️ Error sending message: {e}") + else: + print(f" ⚠️ Channel {mute['channel_id']} not found") + + except discord.Forbidden: + print(f" ❌ Permission denied - bot cannot remove roles") + except Exception as e: + print(f" ❌ Error unmuting {member}: {type(e).__name__}: {e}") + # Don't remove from DB if unmute failed + continue + else: + print(f" ⚠️ User doesn't have mute role (maybe already manually unmuted?)") + + # Remove from DB only if we got this far + print(f" Removing from database...") + self.c.execute("DELETE FROM mutes WHERE user_id = ? AND guild_id = ?", + (mute["user_id"], mute["guild_id"])) + self.conn.commit() + print(f" βœ… Removed mute record from database") + else: + time_left = unmute_time - now + print(f" ⏳ Mute still active. Time remaining: {time_left}") + + @check_mutes.before_loop + async def before_check_mutes(self): + await self.bot.wait_until_ready() + # Wait an extra 5 seconds for guild cache to fully populate + await asyncio.sleep(5) + print("βœ… check_mutes task started (after cache warmup)") + async def setup(bot: commands.Bot): - await bot.add_cog(ModerationBase(bot)) + await bot.add_cog(ModerationBase(bot)) \ No newline at end of file diff --git a/moderation/mute.py b/moderation/mute.py index f0267e7..4a1bab8 100644 --- a/moderation/mute.py +++ b/moderation/mute.py @@ -1,40 +1,46 @@ import discord from discord.ext import commands from discord.ui import View, Button -import asyncio import re -from datetime import timedelta +from datetime import datetime, timedelta from .loader import ModerationBase MUTE_ROLE_ID = 982702037517090836 class MuteCommand(ModerationBase): - @commands.command(name="mute") @ModerationBase.is_admin() async def mute(self, ctx, user: discord.Member, duration: str, *, reason: str = None): - """Mute a user for a duration with confirmation and log infraction""" - # Parse duration - match = re.match(r"(\d+)([wdh])", duration.lower()) + """Mute a user for a duration with confirmation and log infraction.""" + + # Parse duration (1w, 5d, 48h, 30m) + match = re.match(r"(\d+)([wdhm])", duration.lower()) if not match: - await ctx.send("Invalid duration format. Use **1w**, **5d**, **48h**, etc.") + await ctx.send("Invalid duration format. Use **1w**, **5d**, **48h**, **30m**, etc.") return value, unit = match.groups() value = int(value) + if unit == "w": delta = timedelta(weeks=value) elif unit == "d": delta = timedelta(days=value) elif unit == "h": delta = timedelta(hours=value) + elif unit == "m": + delta = timedelta(minutes=value) + else: + await ctx.send("Invalid time unit. Use w/d/h/m.") + return + # Confirmation view view = View(timeout=30) confirmed = {"value": False} async def yes_callback(interaction: discord.Interaction): if interaction.user != ctx.author: - await interaction.response.send_message("You can’t confirm this action.", ephemeral=True) + await interaction.response.send_message("You can't confirm this action.", ephemeral=True) return confirmed["value"] = True await interaction.response.edit_message(content="βœ… Confirmed.", view=None) @@ -42,7 +48,7 @@ class MuteCommand(ModerationBase): async def no_callback(interaction: discord.Interaction): if interaction.user != ctx.author: - await interaction.response.send_message("You can’t cancel this action.", ephemeral=True) + await interaction.response.send_message("You can't cancel this action.", ephemeral=True) return confirmed["value"] = False await interaction.response.edit_message(content="❌ Cancelled.", view=None) @@ -52,12 +58,16 @@ class MuteCommand(ModerationBase): no_button = Button(label="No", style=discord.ButtonStyle.red) yes_button.callback = yes_callback no_button.callback = no_callback - view.add_item(yes_button) view.add_item(no_button) - await ctx.send(f"Are you sure you want to mute {user.mention} for **{duration}**? Reason: {reason or 'No reason provided'}", view=view) + await ctx.send( + f"Are you sure you want to mute {user.mention} for **{duration}**? " + f"Reason: {reason or 'No reason provided'}", + view=view + ) await view.wait() + if not confirmed["value"]: return @@ -66,26 +76,35 @@ class MuteCommand(ModerationBase): await ctx.send("Mute role not found in server.") return + # Add mute role await user.add_roles(mute_role, reason=reason) + # Try to DM the user try: - await user.send(f"You have been muted in **{ctx.guild.name}** for **{duration}**.\nReason: {reason or 'No reason provided'}") + await user.send( + f"You have been muted in **{ctx.guild.name}** for **{duration}**.\n" + f"Reason: {reason or 'No reason provided'}" + ) except: await ctx.send("Could not DM the user.") + # Log infraction await self.log_infraction(ctx.guild.id, user.id, ctx.author.id, "mute", reason) + await ctx.send(f"{user.mention} has been muted for **{duration}**.") - # Schedule unmute - async def unmute_later(): - await asyncio.sleep(delta.total_seconds()) - try: - await user.remove_roles(mute_role, reason="Mute duration expired") - await ctx.send(f"{user.mention} has been unmuted (duration expired).") - except: - pass - - asyncio.create_task(unmute_later()) + # Store mute in DB for auto-unmute (fixed to use consistent data types) + unmute_time = datetime.utcnow() + delta + try: + self.c.execute(""" + INSERT OR REPLACE INTO mutes (user_id, guild_id, unmute_time, channel_id) + VALUES (?, ?, ?, ?) + """, (user.id, ctx.guild.id, unmute_time.isoformat(), ctx.channel.id)) + self.conn.commit() + print(f"βœ… Stored mute for {user} until {unmute_time.isoformat()}") + except Exception as e: + print(f"❌ Failed to insert mute into DB: {e}") + await ctx.send(f"⚠️ Warning: Auto-unmute may not work. Error: {e}") async def setup(bot: commands.Bot): - await bot.add_cog(MuteCommand(bot)) + await bot.add_cog(MuteCommand(bot)) \ No newline at end of file diff --git a/moderation/unmmute.py b/moderation/unmmute.py index 54a9534..23ccaae 100644 --- a/moderation/unmmute.py +++ b/moderation/unmmute.py @@ -1,23 +1,23 @@ import discord from discord.ext import commands from discord.ui import View, Button -import asyncio from .loader import ModerationBase MUTE_ROLE_ID = 982702037517090836 class UnmuteCommand(ModerationBase): - @commands.command(name="unmute") @ModerationBase.is_admin() async def unmute(self, ctx, user: discord.Member): """Unmute a user with confirmation and log infraction""" + + # Confirmation view view = View(timeout=30) confirmed = {"value": False} async def yes_callback(interaction: discord.Interaction): if interaction.user != ctx.author: - await interaction.response.send_message("You can’t confirm this action.", ephemeral=True) + await interaction.response.send_message("You can't confirm this action.", ephemeral=True) return confirmed["value"] = True await interaction.response.edit_message(content="βœ… Confirmed.", view=None) @@ -25,7 +25,7 @@ class UnmuteCommand(ModerationBase): async def no_callback(interaction: discord.Interaction): if interaction.user != ctx.author: - await interaction.response.send_message("You can’t cancel this action.", ephemeral=True) + await interaction.response.send_message("You can't cancel this action.", ephemeral=True) return confirmed["value"] = False await interaction.response.edit_message(content="❌ Cancelled.", view=None) @@ -35,12 +35,12 @@ class UnmuteCommand(ModerationBase): no_button = Button(label="No", style=discord.ButtonStyle.red) yes_button.callback = yes_callback no_button.callback = no_callback - view.add_item(yes_button) view.add_item(no_button) await ctx.send(f"Are you sure you want to unmute {user.mention}?", view=view) await view.wait() + if not confirmed["value"]: return @@ -51,14 +51,22 @@ class UnmuteCommand(ModerationBase): if mute_role in user.roles: await user.remove_roles(mute_role, reason="Unmute issued by command") + try: await user.send(f"You have been **unmuted** in **{ctx.guild.name}**.") except: await ctx.send("Could not DM the user.") + + # Remove from mutes table (using inherited connection from ModerationBase) + self.c.execute("DELETE FROM mutes WHERE user_id = ? AND guild_id = ?", + (user.id, ctx.guild.id)) + self.conn.commit() + print(f"βœ… Removed mute record for {user}") + await self.log_infraction(ctx.guild.id, user.id, ctx.author.id, "unmute", "Manual unmute issued") await ctx.send(f"{user.mention} has been unmuted.") else: await ctx.send(f"{user.mention} is not currently muted.") async def setup(bot: commands.Bot): - await bot.add_cog(UnmuteCommand(bot)) + await bot.add_cog(UnmuteCommand(bot)) \ No newline at end of file diff --git a/xp/rank.py b/xp/rank.py index 816331f..03db176 100644 --- a/xp/rank.py +++ b/xp/rank.py @@ -67,8 +67,8 @@ class Rank(commands.Cog): bar = "β–ˆ" * filled + "β–‘" * (bar_length - filled) # estimate messages left - min_msgs = math.ceil(needed / 100) - max_msgs = math.ceil(needed / 50) + min_msgs = math.ceil(math.ceil(needed / 100) / multiplier) + max_msgs = math.ceil(math.ceil(needed / 50) / multiplier) embed = discord.Embed( description=f"✨ **XP** `{xp:,}` (lv. {level})\n"