Improvements to the suggestion feature, making buttons persistent between restarts, allowing me to send a reason for denying a suggestion, and updating suggestionlist to have filtering

This commit is contained in:
Lilac-Rose
2025-10-21 00:12:05 +02:00
parent 60617fdfd2
commit 5f7734e2f5

View File

@@ -4,16 +4,167 @@ from discord.ext import commands
import aiosqlite
from datetime import datetime
import os
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)
def __init__(self, suggestion_id: int, user_id: int, suggestion_text: str, channel_id: int, admin_message_id: Optional[int], bot: commands.Bot):
super().__init__()
self.suggestion_id = suggestion_id
self.user_id = user_id
self.suggestion_text = suggestion_text
self.channel_id = channel_id
self.admin_message_id = admin_message_id
self.bot = bot
async def on_submit(self, interaction: discord.Interaction):
reason_text = self.reason.value or None
db_path = os.path.join(os.path.dirname(__file__), "suggestions.db")
async with aiosqlite.connect(db_path) as db:
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**."
if reason_text:
dm_note += f"\n**Reason:** {reason_text}"
await user.send(dm_note)
except:
pass
# Notify original channel
channel = self.bot.get_channel(self.channel_id)
if channel:
try:
msg = f"❌ Suggestion **#{self.suggestion_id}** (`{self.suggestion_text}`) has been **denied**."
if reason_text:
msg += f"\n**Reason:** {reason_text}"
await channel.send(msg)
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:
pass
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
super().__init__(timeout=None)
self.bot = bot
self.suggestion_id = suggestion_id
self.user_id = user_id
self.suggestion_text = suggestion_text
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.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.callback = self.deny
deny_btn.disabled = disabled
self.add_item(deny_btn)
async def approve(self, interaction: discord.Interaction):
if interaction.user.id != ADMIN_ID:
await interaction.response.send_message("❌ You can't approve suggestions.", ephemeral=True)
return
if not self.suggestion_id:
await interaction.response.send_message("⚠️ This button is no longer active.", ephemeral=True)
return
db_path = os.path.join(os.path.dirname(__file__), "suggestions.db")
async with aiosqlite.connect(db_path) as db:
await db.execute("UPDATE suggestions SET status = ? WHERE id = ?", ("Approved", self.suggestion_id))
await db.commit()
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)
dm = admin_user.dm_channel or await admin_user.create_dm()
orig_msg = await dm.fetch_message(self.admin_message_id)
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:
await interaction.message.edit(view=self)
except:
pass
async def deny(self, interaction: discord.Interaction):
if interaction.user.id != ADMIN_ID:
await interaction.response.send_message("❌ You can't deny suggestions.", ephemeral=True)
return
if not self.suggestion_id:
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 Suggestion(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.db_path = os.path.join(os.path.dirname(__file__), "suggestions.db")
self.db = None
async def cog_load(self):
# Connect to database
# 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 (
@@ -21,13 +172,30 @@ class Suggestion(commands.Cog):
user_id INTEGER,
suggestion TEXT,
status TEXT,
channel_id INTEGER
channel_id INTEGER,
reason TEXT,
admin_message_id INTEGER
)
""")
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
async def cog_unload(self):
await self.db.close()
if self.db:
await self.db.close()
@app_commands.command(name="suggest", description="Submit a suggestion")
async def suggest(self, interaction: discord.Interaction, idea: str):
@@ -59,7 +227,18 @@ class Suggestion(commands.Cog):
embed.add_field(name="Channel", value=f"<#{interaction.channel_id}>")
view = SuggestionButtons(self.bot, suggestion_id, interaction.user.id, idea, interaction.channel_id)
await admin.send(embed=embed, view=view)
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:
pass
except Exception as e:
print(f"Failed to send DM to admin: {e}")
@@ -107,117 +286,41 @@ class Suggestion(commands.Cog):
if channel:
await channel.send(f"🎉 Suggestion **#{suggestion_id}** (`{suggestion_text}`) has been marked as **completed!**")
@app_commands.command(name="suggestion_list", description="List all suggestions (admin only)")
async def suggestion_list(self, interaction: discord.Interaction):
if interaction.user.id != ADMIN_ID:
await interaction.response.send_message("❌ You don't have permission to view this.", ephemeral=True)
return
@app_commands.command(name="suggestion_list", description="List suggestions (use status filter to narrow)")
@app_commands.choices(status=[
app_commands.Choice(name="All", value="All"),
app_commands.Choice(name="Pending", value="Pending"),
app_commands.Choice(name="Approved", value="Approved"),
app_commands.Choice(name="Denied", value="Denied"),
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"
async with self.db.execute("SELECT id, user_id, suggestion, status FROM suggestions ORDER BY id DESC") as cursor:
if selected == "All":
query = "SELECT id, user_id, suggestion, status FROM suggestions ORDER BY id DESC"
params = ()
else:
query = "SELECT id, user_id, suggestion, status FROM suggestions WHERE status = ? ORDER BY id DESC"
params = (selected,)
async with self.db.execute(query, params) as cursor:
rows = await cursor.fetchall()
if not rows:
await interaction.response.send_message("No suggestions found.", ephemeral=True)
await interaction.response.send_message("No suggestions found.", ephemeral=False)
return
embed = discord.Embed(title="📋 Suggestions", color=discord.Color.green())
for sid, uid, suggestion, status in rows:
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: {status}",
value=f"<@{uid}> — {suggestion[:100]}{'...' if len(suggestion) > 100 else ''}",
name=f"ID: {sid} | Status: {st}",
value=f"<@{uid}> — {suggestion_text[:100]}{'...' if len(suggestion_text) > 100 else ''}",
inline=False
)
await interaction.response.send_message(embed=embed, ephemeral=True)
class SuggestionButtons(discord.ui.View):
def __init__(self, bot, suggestion_id=None, user_id=None, suggestion_text=None, channel_id=None):
super().__init__(timeout=None)
self.bot = bot
self.suggestion_id = suggestion_id
self.user_id = user_id
self.suggestion_text = suggestion_text
self.channel_id = channel_id
approve_btn = discord.ui.Button(
label="Approve ✅",
style=discord.ButtonStyle.success,
custom_id=f"suggest_approve_{suggestion_id or 'dummy'}"
)
approve_btn.callback = self.approve
self.add_item(approve_btn)
deny_btn = discord.ui.Button(
label="Deny ❌",
style=discord.ButtonStyle.danger,
custom_id=f"suggest_deny_{suggestion_id or 'dummy'}"
)
deny_btn.callback = self.deny
self.add_item(deny_btn)
async def approve(self, interaction: discord.Interaction):
if interaction.user.id != ADMIN_ID:
await interaction.response.send_message("❌ You can't approve suggestions.", ephemeral=True)
return
if not self.suggestion_id:
await interaction.response.send_message("⚠️ This button is no longer active.", ephemeral=True)
return
db_path = os.path.join(os.path.dirname(__file__), "suggestions.db")
async with aiosqlite.connect(db_path) as db:
await db.execute("UPDATE suggestions SET status = ? WHERE id = ?", ("Approved", self.suggestion_id))
await db.commit()
await interaction.response.send_message(f"✅ Suggestion #{self.suggestion_id} approved.", ephemeral=False)
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
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
for item in self.children:
item.disabled = True
await interaction.message.edit(view=self)
async def deny(self, interaction: discord.Interaction):
if interaction.user.id != ADMIN_ID:
await interaction.response.send_message("❌ You can't deny suggestions.", ephemeral=True)
return
if not self.suggestion_id:
await interaction.response.send_message("⚠️ This button is no longer active.", ephemeral=True)
return
db_path = os.path.join(os.path.dirname(__file__), "suggestions.db")
async with aiosqlite.connect(db_path) as db:
await db.execute("UPDATE suggestions SET status = ? WHERE id = ?", ("Denied", self.suggestion_id))
await db.commit()
await interaction.response.send_message(f"❌ Suggestion #{self.suggestion_id} denied.", ephemeral=False)
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 **denied.**")
except:
pass
channel = self.bot.get_channel(self.channel_id)
if channel:
await channel.send(f"❌ Suggestion **#{self.suggestion_id}** (`{self.suggestion_text}`) has been **denied.**")
# Disable buttons after action
for item in self.children:
item.disabled = True
await interaction.message.edit(view=self)
await interaction.response.send_message(embed=embed, ephemeral=False)
async def setup(bot):