mirror of
https://github.com/Lilac-Rose/Lacie.git
synced 2026-09-30 20:26:40 -05:00
Added suggestion pagination and a daily/weekly/monthly xp lb
This commit is contained in:
224
commands/whitelist.py
Normal file
224
commands/whitelist.py
Normal 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))
|
||||
@@ -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):
|
||||
|
||||
40
xp/add_xp.py
40
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()
|
||||
@@ -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
|
||||
@@ -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
68
xp/reset_task.py
Normal 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))
|
||||
16
xp/utils.py
16
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)
|
||||
Reference in New Issue
Block a user