Updated rank.py to make the message estimator take multiplier into account

This commit is contained in:
Lilac-Rose 2025-10-08 22:08:24 +02:00
parent f8fc664a93
commit d243293ad9
4 changed files with 204 additions and 33 deletions

View File

@ -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))

View File

@ -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 cant 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 cant 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))

View File

@ -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 cant 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 cant 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))

View File

@ -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"