Added suggestion pagination and a daily/weekly/monthly xp lb

This commit is contained in:
Lilac-Rose
2025-11-07 21:31:27 +01:00
parent 38a0c892b4
commit 8e8f79e41e
7 changed files with 443 additions and 73 deletions

224
commands/whitelist.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

68
xp/reset_task.py Normal file
View File

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

View File

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