diff --git a/commands/whitelist.py b/commands/whitelist.py new file mode 100644 index 0000000..0046f3a --- /dev/null +++ b/commands/whitelist.py @@ -0,0 +1,224 @@ +import discord +from discord import app_commands +from discord.ext import commands +import json +import os + +WHITELIST_FILE = "whitelist.json" +ADMIN_ROLE_NAME = "Ritual Member" + +def load_whitelist(): + if not os.path.exists(WHITELIST_FILE): + return { + "Vanilla": {"pending": [], "whitelisted": []}, + "Modded": {"pending": [], "whitelisted": []}, + "Both": {"pending": [], "whitelisted": []} + } + with open(WHITELIST_FILE, "r") as f: + return json.load(f) + +def save_whitelist(data): + with open(WHITELIST_FILE, "w") as f: + json.dump(data, f, indent=4) + +def normalize_username(username: str) -> str: + """Normalize Minecraft username to prevent case-sensitivity issues.""" + return username.strip() + +def has_admin_role(interaction: discord.Interaction) -> bool: + """Check if user has admin permissions.""" + return any(role.name == ADMIN_ROLE_NAME for role in interaction.user.roles) + +class WhitelistRequest(commands.Cog): + def __init__(self, bot): + self.bot = bot + + # --- /whitelistrequest --- + @app_commands.command(name="whitelistrequest", description="Request to be whitelisted on a Minecraft server.") + @app_commands.describe(server="Select which server to request for", username="Your Minecraft username") + @app_commands.choices(server=[ + app_commands.Choice(name="Vanilla", value="Vanilla"), + app_commands.Choice(name="Modded", value="Modded"), + app_commands.Choice(name="Both", value="Both"), + ]) + async def whitelist_request(self, interaction: discord.Interaction, server: app_commands.Choice[str], username: str): + data = load_whitelist() + server_key = server.value + username = normalize_username(username) + + # Validate username (basic check) + if len(username) < 3 or len(username) > 16 or not username.replace("_", "").isalnum(): + await interaction.response.send_message( + f"⚠️ Invalid Minecraft username. Must be 3-16 characters (letters, numbers, underscores only).", + ephemeral=True + ) + return + + # Check all servers for existing requests/whitelists + servers_to_check = ["Vanilla", "Modded", "Both"] if server_key == "Both" else [server_key] + + for s in servers_to_check: + if username in data[s]["pending"]: + await interaction.response.send_message( + f"⚠️ `{username}` already has a pending whitelist request for {s}.", + ephemeral=True + ) + return + if username in data[s]["whitelisted"]: + await interaction.response.send_message( + f"⚠️ `{username}` is already whitelisted on {s}.", + ephemeral=True + ) + return + + # Add request to appropriate server(s) + if server_key == "Both": + data["Vanilla"]["pending"].append(username) + data["Modded"]["pending"].append(username) + else: + data[server_key]["pending"].append(username) + + save_whitelist(data) + + await interaction.response.send_message( + f"📩 Whitelist request for `{username}` added to **{server.name}**.", + ephemeral=False + ) + + # --- /listwhitelist --- + @app_commands.command(name="listwhitelist", description="List whitelist requests and whitelisted players.") + @app_commands.describe(status="Show pending or whitelisted users", server="Filter by server") + @app_commands.choices( + status=[ + app_commands.Choice(name="Pending", value="pending"), + app_commands.Choice(name="Whitelisted", value="whitelisted"), + ], + server=[ + app_commands.Choice(name="Vanilla", value="Vanilla"), + app_commands.Choice(name="Modded", value="Modded"), + app_commands.Choice(name="Both", value="Both"), + ], + ) + async def list_whitelist( + self, + interaction: discord.Interaction, + status: app_commands.Choice[str] = None, + server: app_commands.Choice[str] = None + ): + data = load_whitelist() + msg = "📋 **Whitelist Requests**\n\n" + + servers_to_show = [server.value] if server else ["Vanilla", "Modded"] + status_to_show = [status.value] if status else ["pending", "whitelisted"] + + for s in servers_to_show: + msg += f"**{s} Server:**\n" + for st in status_to_show: + users = data[s][st] + if users: + msg += f" • **{st.title()}**: {', '.join(users)}\n" + else: + msg += f" • **{st.title()}**: *(none)*\n" + msg += "\n" + + await interaction.response.send_message(msg, ephemeral=False) + + # --- /markwhitelisted --- + @app_commands.command(name="markwhitelisted", description="[ADMIN] Mark a pending user as whitelisted.") + @app_commands.describe(server="Select which server", username="Minecraft username to mark as whitelisted") + @app_commands.choices(server=[ + app_commands.Choice(name="Vanilla", value="Vanilla"), + app_commands.Choice(name="Modded", value="Modded"), + app_commands.Choice(name="Both", value="Both"), + ]) + async def mark_whitelisted(self, interaction: discord.Interaction, server: app_commands.Choice[str], username: str): + if not has_admin_role(interaction): + await interaction.response.send_message("⛔ You need the Admin role to use this command.", ephemeral=True) + return + + data = load_whitelist() + server_key = server.value + username = normalize_username(username) + + servers_to_update = ["Vanilla", "Modded"] if server_key == "Both" else [server_key] + updated = [] + + for s in servers_to_update: + if username in data[s]["pending"]: + data[s]["pending"].remove(username) + if username not in data[s]["whitelisted"]: + data[s]["whitelisted"].append(username) + updated.append(s) + + if not updated: + await interaction.response.send_message( + f"⚠️ `{username}` is not pending for {server.name}.", + ephemeral=True + ) + return + + save_whitelist(data) + servers_str = " and ".join(updated) + await interaction.response.send_message( + f"✅ `{username}` has been marked as whitelisted on **{servers_str}**.", + ephemeral=False + ) + + # --- /removewhitelist --- + @app_commands.command(name="removewhitelist", description="[ADMIN] Remove a user from whitelist or reject a request.") + @app_commands.describe( + server="Select which server", + username="Minecraft username to remove", + remove_from="Remove from pending or whitelisted list" + ) + @app_commands.choices( + server=[ + app_commands.Choice(name="Vanilla", value="Vanilla"), + app_commands.Choice(name="Modded", value="Modded"), + app_commands.Choice(name="Both", value="Both"), + ], + remove_from=[ + app_commands.Choice(name="Pending", value="pending"), + app_commands.Choice(name="Whitelisted", value="whitelisted"), + ] + ) + async def remove_whitelist( + self, + interaction: discord.Interaction, + server: app_commands.Choice[str], + username: str, + remove_from: app_commands.Choice[str] + ): + if not has_admin_role(interaction): + await interaction.response.send_message("⛔ You need the Admin role to use this command.", ephemeral=True) + return + + data = load_whitelist() + server_key = server.value + username = normalize_username(username) + list_type = remove_from.value + + servers_to_update = ["Vanilla", "Modded"] if server_key == "Both" else [server_key] + removed = [] + + for s in servers_to_update: + if username in data[s][list_type]: + data[s][list_type].remove(username) + removed.append(s) + + if not removed: + await interaction.response.send_message( + f"⚠️ `{username}` is not in the {list_type} list for {server.name}.", + ephemeral=True + ) + return + + save_whitelist(data) + servers_str = " and ".join(removed) + await interaction.response.send_message( + f"🗑️ `{username}` has been removed from the {list_type} list on **{servers_str}**.", + ephemeral=False + ) + +async def setup(bot): + await bot.add_cog(WhitelistRequest(bot)) \ No newline at end of file diff --git a/suggestion/suggest.py b/suggestion/suggest.py index 179958e..86cc85e 100644 --- a/suggestion/suggest.py +++ b/suggestion/suggest.py @@ -8,6 +8,7 @@ from typing import Optional ADMIN_ID = 252130669919076352 + class DenyModal(discord.ui.Modal, title="Reason for denying suggestion"): reason = discord.ui.TextInput(label="Reason (optional)", style=discord.TextStyle.long, required=False, max_length=2000) @@ -28,10 +29,8 @@ class DenyModal(discord.ui.Modal, title="Reason for denying suggestion"): await db.execute("UPDATE suggestions SET status = ?, reason = ? WHERE id = ?", ("Denied", reason_text, self.suggestion_id)) await db.commit() - # Respond to modal submit await interaction.response.send_message(f"❌ Suggestion #{self.suggestion_id} denied.", ephemeral=False) - # Notify user try: user = await self.bot.fetch_user(self.user_id) dm_note = f"❌ Your suggestion (ID: {self.suggestion_id}) — `{self.suggestion_text}` has been **denied**." @@ -41,7 +40,6 @@ class DenyModal(discord.ui.Modal, title="Reason for denying suggestion"): except: pass - # Notify original channel channel = self.bot.get_channel(self.channel_id) if channel: try: @@ -52,13 +50,11 @@ class DenyModal(discord.ui.Modal, title="Reason for denying suggestion"): except: pass - # Disable buttons on admin message (if we have the message id) if self.admin_message_id: try: admin_user = await self.bot.fetch_user(ADMIN_ID) dm = admin_user.dm_channel or await admin_user.create_dm() orig_msg = await dm.fetch_message(self.admin_message_id) - # Create a view with same custom_ids but disabled disabled_view = SuggestionButtons(self.bot, suggestion_id=self.suggestion_id, user_id=self.user_id, suggestion_text=self.suggestion_text, channel_id=self.channel_id, admin_message_id=self.admin_message_id, disabled=True) await orig_msg.edit(view=disabled_view) except Exception: @@ -66,8 +62,7 @@ class DenyModal(discord.ui.Modal, title="Reason for denying suggestion"): class SuggestionButtons(discord.ui.View): - def __init__(self, bot, suggestion_id=None, user_id=None, suggestion_text=None, channel_id=None, admin_message_id: Optional[int]=None, disabled: bool=False): - # timeout=None to make persistent + def __init__(self, bot, suggestion_id=None, user_id=None, suggestion_text=None, channel_id=None, admin_message_id: Optional[int] = None, disabled: bool = False): super().__init__(timeout=None) self.bot = bot self.suggestion_id = suggestion_id @@ -76,26 +71,15 @@ class SuggestionButtons(discord.ui.View): self.channel_id = channel_id self.admin_message_id = admin_message_id - # custom_id includes message id so callbacks can find the message later; message id may be None for re-registering but that's fine approve_cid = f"suggest_approve_{suggestion_id}_{admin_message_id or 0}" deny_cid = f"suggest_deny_{suggestion_id}_{admin_message_id or 0}" - approve_btn = discord.ui.Button( - label="Approve ✅", - style=discord.ButtonStyle.success, - custom_id=approve_cid - ) + approve_btn = discord.ui.Button(label="Approve ✅", style=discord.ButtonStyle.success, custom_id=approve_cid, disabled=disabled) approve_btn.callback = self.approve - approve_btn.disabled = disabled self.add_item(approve_btn) - deny_btn = discord.ui.Button( - label="Deny ❌", - style=discord.ButtonStyle.danger, - custom_id=deny_cid - ) + deny_btn = discord.ui.Button(label="Deny ❌", style=discord.ButtonStyle.danger, custom_id=deny_cid, disabled=disabled) deny_btn.callback = self.deny - deny_btn.disabled = disabled self.add_item(deny_btn) async def approve(self, interaction: discord.Interaction): @@ -114,19 +98,16 @@ class SuggestionButtons(discord.ui.View): await interaction.response.send_message(f"✅ Suggestion #{self.suggestion_id} approved.", ephemeral=False) - # Notify user try: user = await self.bot.fetch_user(self.user_id) await user.send(f"✅ Your suggestion (ID: {self.suggestion_id}) — `{self.suggestion_text}` has been **approved!**") except: pass - # Notify original channel channel = self.bot.get_channel(self.channel_id) if channel: await channel.send(f"✅ Suggestion **#{self.suggestion_id}** (`{self.suggestion_text}`) has been **approved!**") - # Disable buttons after action by editing the admin message (if present) if self.admin_message_id: try: admin_user = await self.bot.fetch_user(ADMIN_ID) @@ -135,7 +116,6 @@ class SuggestionButtons(discord.ui.View): disabled_view = SuggestionButtons(self.bot, suggestion_id=self.suggestion_id, user_id=self.user_id, suggestion_text=self.suggestion_text, channel_id=self.channel_id, admin_message_id=self.admin_message_id, disabled=True) await orig_msg.edit(view=disabled_view) except Exception: - # fallback: try to disable children of THIS view and edit message (if interaction.message exists) for item in self.children: item.disabled = True try: @@ -152,11 +132,47 @@ class SuggestionButtons(discord.ui.View): await interaction.response.send_message("⚠️ This button is no longer active.", ephemeral=True) return - # Show a modal to capture reason modal = DenyModal(suggestion_id=self.suggestion_id, user_id=self.user_id, suggestion_text=self.suggestion_text, channel_id=self.channel_id, admin_message_id=self.admin_message_id, bot=self.bot) await interaction.response.send_modal(modal) +class PaginationView(discord.ui.View): + def __init__(self, embeds, user: discord.User): + super().__init__(timeout=180) + self.embeds = embeds + self.user = user + self.current_page = 0 + + if len(embeds) == 1: + self.previous_button.disabled = True + self.next_button.disabled = True + else: + self.previous_button.disabled = True + + async def update_page(self, interaction: discord.Interaction): + self.previous_button.disabled = self.current_page == 0 + self.next_button.disabled = self.current_page == len(self.embeds) - 1 + await interaction.response.edit_message(embed=self.embeds[self.current_page], view=self) + + @discord.ui.button(label="◀️ Previous", style=discord.ButtonStyle.secondary) + async def previous_button(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user != self.user: + await interaction.response.send_message("You can’t control this pagination.", ephemeral=True) + return + if self.current_page > 0: + self.current_page -= 1 + await self.update_page(interaction) + + @discord.ui.button(label="Next ▶️", style=discord.ButtonStyle.secondary) + async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user != self.user: + await interaction.response.send_message("You can’t control this pagination.", ephemeral=True) + return + if self.current_page < len(self.embeds) - 1: + self.current_page += 1 + await self.update_page(interaction) + + class Suggestion(commands.Cog): def __init__(self, bot): self.bot = bot @@ -164,7 +180,6 @@ class Suggestion(commands.Cog): self.db = None async def cog_load(self): - # Connect to database and ensure schema (including reason and admin_message_id) self.db = await aiosqlite.connect(self.db_path) await self.db.execute(""" CREATE TABLE IF NOT EXISTS suggestions ( @@ -179,16 +194,12 @@ class Suggestion(commands.Cog): """) await self.db.commit() - # Register persistent views for pending suggestions so buttons keep working after restarts. async with self.db.execute("SELECT id, user_id, suggestion, channel_id, admin_message_id FROM suggestions WHERE status = ?", ("Pending",)) as cursor: rows = await cursor.fetchall() for sid, uid, suggestion_text, channel_id, admin_msg_id in rows: - # Only re-register views if we have an admin_message_id (message was sent) - # but even if admin_msg_id is None, we register a view with admin_message_id None -> custom_id uses 0 v = SuggestionButtons(self.bot, suggestion_id=sid, user_id=uid, suggestion_text=suggestion_text, channel_id=channel_id, admin_message_id=admin_msg_id) try: - # registers the view for persistent components handling self.bot.add_view(v) except Exception: pass @@ -200,7 +211,6 @@ class Suggestion(commands.Cog): @app_commands.command(name="suggest", description="Submit a suggestion") async def suggest(self, interaction: discord.Interaction, idea: str): try: - # Insert suggestion into DB await self.db.execute( "INSERT INTO suggestions (user_id, suggestion, status, channel_id) VALUES (?, ?, ?, ?)", (interaction.user.id, idea, "Pending", interaction.channel_id) @@ -210,11 +220,8 @@ class Suggestion(commands.Cog): async with self.db.execute("SELECT last_insert_rowid()") as cursor: suggestion_id = (await cursor.fetchone())[0] - await interaction.response.send_message( - f"✅ Suggestion submitted! (ID: **{suggestion_id}**)\n> {idea}" - ) + await interaction.response.send_message(f"✅ Suggestion submitted! (ID: **{suggestion_id}**)\n> {idea}") - # Send DM to admin with persistent buttons try: admin = await self.bot.fetch_user(ADMIN_ID) embed = discord.Embed( @@ -229,11 +236,9 @@ class Suggestion(commands.Cog): view = SuggestionButtons(self.bot, suggestion_id, interaction.user.id, idea, interaction.channel_id) sent = await admin.send(embed=embed, view=view) - # store admin message id so we can re-register persistent view for it later and edit it await self.db.execute("UPDATE suggestions SET admin_message_id = ? WHERE id = ?", (sent.id, suggestion_id)) await self.db.commit() - # Register the view so the component interactions will be handled (persistent) try: self.bot.add_view(SuggestionButtons(self.bot, suggestion_id, interaction.user.id, idea, interaction.channel_id, admin_message_id=sent.id)) except Exception: @@ -255,9 +260,7 @@ class Suggestion(commands.Cog): await interaction.response.send_message("❌ You don't have permission to do that.", ephemeral=True) return - async with self.db.execute( - "SELECT user_id, suggestion, status, channel_id FROM suggestions WHERE id = ?", (suggestion_id,) - ) as cursor: + async with self.db.execute("SELECT user_id, suggestion, status, channel_id FROM suggestions WHERE id = ?", (suggestion_id,)) as cursor: row = await cursor.fetchone() if not row: @@ -274,14 +277,12 @@ class Suggestion(commands.Cog): await interaction.response.send_message(f"✅ Suggestion #{suggestion_id} marked as completed!", ephemeral=False) - # Notify user try: user = await self.bot.fetch_user(user_id) await user.send(f"🎉 Your suggestion (ID: {suggestion_id}) — `{suggestion_text}` has been **implemented!**") except: pass - # Notify original channel channel = self.bot.get_channel(channel_id) if channel: await channel.send(f"🎉 Suggestion **#{suggestion_id}** (`{suggestion_text}`) has been marked as **completed!**") @@ -295,7 +296,6 @@ class Suggestion(commands.Cog): app_commands.Choice(name="Completed", value="Completed") ]) async def suggestion_list(self, interaction: discord.Interaction, status: app_commands.Choice[str]): - # This command is intentionally not admin-restricted and not ephemeral (per request). selected = status.value if status else "All" if selected == "All": @@ -312,15 +312,20 @@ class Suggestion(commands.Cog): await interaction.response.send_message("No suggestions found.", ephemeral=False) return - embed = discord.Embed(title=f"📋 Suggestions — {selected}", color=discord.Color.green()) - for sid, uid, suggestion_text, st in rows: - embed.add_field( - name=f"ID: {sid} | Status: {st}", - value=f"<@{uid}> — {suggestion_text[:100]}{'...' if len(suggestion_text) > 100 else ''}", - inline=False - ) + embeds = [] + per_page = 10 + for i in range(0, len(rows), per_page): + embed = discord.Embed(title=f"📋 Suggestions — {selected} (Page {i//per_page + 1})", color=discord.Color.green()) + for sid, uid, suggestion_text, st in rows[i:i+per_page]: + embed.add_field( + name=f"ID: {sid} | Status: {st}", + value=f"<@{uid}> — {suggestion_text[:100]}{'...' if len(suggestion_text) > 100 else ''}", + inline=False + ) + embeds.append(embed) - await interaction.response.send_message(embed=embed, ephemeral=False) + view = PaginationView(embeds, interaction.user) + await interaction.response.send_message(embed=embeds[0], view=view, ephemeral=False) async def setup(bot): diff --git a/xp/add_xp.py b/xp/add_xp.py index 1622b62..e389dfa 100644 --- a/xp/add_xp.py +++ b/xp/add_xp.py @@ -4,41 +4,55 @@ from .utils import get_multiplier, random_xp, can_get_xp, check_level_up from .exclude_channels import is_channel_excluded async def add_xp(member): - if hasattr(member, "guild"): last_message = getattr(member, "last_message", None) if last_message and getattr(last_message, "channel", None): if is_channel_excluded(last_message.channel.id): return + # Check cooldown ONCE before adding to any database + conn_check, cur_check = get_db("lifetime") + cur_check.execute("SELECT last_message FROM xp WHERE user_id = ?", (str(member.id),)) + row_check = cur_check.fetchone() + conn_check.close() + + if row_check and not can_get_xp(row_check[0]): + return # Still on cooldown, don't add XP to any database + base_xp = random_xp() - - for lifetime in (True, False): # True = lifetime, False = annual - conn, cur = get_db(lifetime) + + # List of all leaderboard types to update + leaderboard_types = [ + ("lifetime", True), # (db_type, apply_multiplier) + ("annual", False), + ("monthly", False), + ("weekly", False), + ("daily", False) + ] + + for db_type, apply_multiplier in leaderboard_types: + conn, cur = get_db(db_type) cur.execute("SELECT xp, level, last_message FROM xp WHERE user_id = ?", (str(member.id),)) row = cur.fetchone() - + if row: xp, level, last_msg = row - if not can_get_xp(last_msg): - conn.close() - continue else: xp, level, last_msg = (0, 0, 0) cur.execute( "INSERT INTO xp (user_id, xp, level, last_message) VALUES (?, ?, ?, ?)", (str(member.id), 0, 0, 0) ) - + # Only apply multiplier for lifetime XP - gained = int(base_xp * get_multiplier(member, apply_multiplier=lifetime)) + gained = int(base_xp * get_multiplier(member, apply_multiplier=apply_multiplier)) new_xp = xp + gained - + cur.execute( "UPDATE xp SET xp = ?, last_message = ? WHERE user_id = ?", (new_xp, int(time.time()), str(member.id)) ) conn.commit() - - await check_level_up(member, cur, conn, lifetime) + + await check_level_up(member, cur, conn, db_type == "lifetime") conn.close() \ No newline at end of file diff --git a/xp/database.py b/xp/database.py index 769b25f..627a31b 100644 --- a/xp/database.py +++ b/xp/database.py @@ -3,11 +3,28 @@ import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -def get_db(lifetime=True): - db_name = "lifetime.db" if lifetime else "annual.db" +def get_db(db_type="lifetime"): + """ + Get database connection based on type. + + Args: + db_type: "lifetime", "annual", "monthly", "weekly", or "daily" + Can also be a boolean (True for lifetime, False for annual) for backwards compatibility + """ + # Handle backwards compatibility with boolean argument + if isinstance(db_type, bool): + db_type = "lifetime" if db_type else "annual" + + valid_types = ["lifetime", "annual", "monthly", "weekly", "daily"] + if db_type not in valid_types: + db_type = "lifetime" + + db_name = f"{db_type}.db" db_path = os.path.join(BASE_DIR, db_name) conn = sqlite3.connect(db_path) cur = conn.cursor() + + # Only create tables if they don't exist (won't modify existing tables) cur.execute(""" CREATE TABLE IF NOT EXISTS xp ( user_id TEXT PRIMARY KEY, @@ -16,5 +33,35 @@ def get_db(lifetime=True): last_message INTEGER DEFAULT 0 ) """) + + # Store last reset time for time-based leaderboards ONLY + # This won't affect lifetime or annual databases + if db_type in ["daily", "weekly", "monthly"]: + cur.execute(""" + CREATE TABLE IF NOT EXISTS reset_log ( + id INTEGER PRIMARY KEY, + last_reset INTEGER DEFAULT 0 + ) + """) + cur.execute("INSERT OR IGNORE INTO reset_log (id, last_reset) VALUES (1, 0)") + conn.commit() return conn, cur + +def reset_leaderboard(db_type): + """Reset a time-based leaderboard.""" + import time + + conn, cur = get_db(db_type) + cur.execute("DELETE FROM xp") + cur.execute("UPDATE reset_log SET last_reset = ? WHERE id = 1", (int(time.time()),)) + conn.commit() + conn.close() + +def get_last_reset(db_type): + """Get the last reset time for a leaderboard.""" + conn, cur = get_db(db_type) + cur.execute("SELECT last_reset FROM reset_log WHERE id = 1") + row = cur.fetchone() + conn.close() + return row[0] if row else 0 \ No newline at end of file diff --git a/xp/leaderboard.py b/xp/leaderboard.py index 244b37f..a327b01 100644 --- a/xp/leaderboard.py +++ b/xp/leaderboard.py @@ -60,7 +60,10 @@ class Leaderboard(commands.Cog): @app_commands.choices( board_type=[ app_commands.Choice(name="Lifetime XP", value="lifetime"), - app_commands.Choice(name="Annual XP", value="annual") + app_commands.Choice(name="Annual XP", value="annual"), + app_commands.Choice(name="Monthly XP", value="monthly"), + app_commands.Choice(name="Weekly XP", value="weekly"), + app_commands.Choice(name="Daily XP", value="daily") ] ) async def leaderboard( @@ -72,10 +75,9 @@ class Leaderboard(commands.Cog): await interaction.response.defer(thinking=True) board_type_value = board_type.value if board_type else "lifetime" - board_display_name = board_type.name if board_type else "Lifetime" + board_display_name = board_type.name if board_type else "Lifetime XP" - use_lifetime_db = board_type_value == "lifetime" - conn, cur = get_db(use_lifetime_db) + conn, cur = get_db(board_type_value) cur.execute("SELECT user_id, xp, level FROM xp ORDER BY xp DESC") all_rows = cur.fetchall() diff --git a/xp/reset_task.py b/xp/reset_task.py new file mode 100644 index 0000000..5a79483 --- /dev/null +++ b/xp/reset_task.py @@ -0,0 +1,68 @@ +from discord.ext import tasks, commands +from datetime import datetime, timezone +from .database import reset_leaderboard, get_last_reset +import calendar + +class ResetTask(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.check_resets.start() + + def cog_unload(self): + self.check_resets.cancel() + + @tasks.loop(minutes=1) + async def check_resets(self): + """Check if any leaderboards need to be reset.""" + now = datetime.now(timezone.utc) + current_time = int(now.timestamp()) + + # Daily reset - at midnight UTC + if self.should_reset_daily(now): + last_reset = get_last_reset("daily") + # Check if we haven't reset today yet + last_reset_date = datetime.fromtimestamp(last_reset, timezone.utc).date() if last_reset > 0 else None + if last_reset_date != now.date(): + reset_leaderboard("daily") + print(f"[XP System] Daily leaderboard reset at {now}") + + # Weekly reset - Sunday at midnight UTC + if self.should_reset_weekly(now): + last_reset = get_last_reset("weekly") + last_reset_week = datetime.fromtimestamp(last_reset, timezone.utc).isocalendar()[1] if last_reset > 0 else None + current_week = now.isocalendar()[1] + if last_reset_week != current_week: + reset_leaderboard("weekly") + print(f"[XP System] Weekly leaderboard reset at {now}") + + # Monthly reset - Last day of month at midnight UTC + if self.should_reset_monthly(now): + last_reset = get_last_reset("monthly") + last_reset_month = datetime.fromtimestamp(last_reset, timezone.utc).month if last_reset > 0 else None + if last_reset_month != now.month: + reset_leaderboard("monthly") + print(f"[XP System] Monthly leaderboard reset at {now}") + + def should_reset_daily(self, now): + """Check if it's time for daily reset (00:00 UTC).""" + return now.hour == 0 and now.minute == 0 + + def should_reset_weekly(self, now): + """Check if it's time for weekly reset (Sunday 00:00 UTC).""" + # weekday() returns 6 for Sunday + return now.weekday() == 6 and now.hour == 0 and now.minute == 0 + + def should_reset_monthly(self, now): + """Check if it's time for monthly reset (last day of month 00:00 UTC).""" + # Get last day of current month + last_day = calendar.monthrange(now.year, now.month)[1] + return now.day == last_day and now.hour == 0 and now.minute == 0 + + @check_resets.before_loop + async def before_check_resets(self): + """Wait until the bot is ready before starting the loop.""" + await self.bot.wait_until_ready() + +async def setup(bot): + """Setup function to add the reset task cog.""" + await bot.add_cog(ResetTask(bot)) \ No newline at end of file diff --git a/xp/utils.py b/xp/utils.py index 5ba2db8..403ca81 100644 --- a/xp/utils.py +++ b/xp/utils.py @@ -1,6 +1,10 @@ -import math, random, time, json +import math +import random +import time +import json from discord.utils import get from pathlib import Path + CONFIG_PATH = Path("/home/lilacrose/lilacrose.dev2.0/bots/lacie/xp_config.json") def load_config(): @@ -38,21 +42,27 @@ def can_get_xp(last_message_time: int) -> bool: async def check_level_up(member, cur, conn, lifetime=True): config = load_config() - role_rewards = {int(k): v for k,v in config["ROLE_REWARDS"].items()} + role_rewards = {int(k): int(v) for k, v in config["ROLE_REWARDS"].items()} + cur.execute("SELECT xp, level FROM xp WHERE user_id = ?", (str(member.id),)) row = cur.fetchone() if not row: return + xp, level = row new_level = level + while xp >= xp_for_level(new_level + 1): new_level += 1 + if new_level > level: cur.execute("UPDATE xp SET level = ? WHERE user_id = ?", (new_level, str(member.id))) conn.commit() + if lifetime: + # Grant all roles for levels they've reached for lvl, role_id in role_rewards.items(): if new_level >= lvl: role = get(member.guild.roles, id=role_id) - if role: + if role and role not in member.roles: await member.add_roles(role) \ No newline at end of file