diff --git a/commands/connect4.py b/commands/connect4.py index c74fc87..8b6dc97 100644 --- a/commands/connect4.py +++ b/commands/connect4.py @@ -9,7 +9,6 @@ EMPTY = "⚪" RED = "🔴" YELLOW = "🟡" - class ColumnButton(discord.ui.Button): def __init__(self, col: int): super().__init__( diff --git a/commands/help.py b/commands/help.py deleted file mode 100644 index 0167313..0000000 --- a/commands/help.py +++ /dev/null @@ -1,249 +0,0 @@ -import discord -from discord.ext import commands -from discord import app_commands -from typing import Optional -import difflib - -class Help(commands.Cog): - def __init__(self, bot): - self.bot = bot - - def get_command_signature(self, command): - """Get the usage signature for a command""" - if isinstance(command, commands.HybridCommand): - # For hybrid commands, show both formats - params = [] - for name, param in command.clean_params.items(): - if param.default == param.empty: - params.append(f"<{name}>") - else: - params.append(f"[{name}]") - param_str = " ".join(params) - return f"!{command.name} {param_str}\n/{command.name} {param_str}" - else: - return f"{command.qualified_name} {command.signature}" - - def get_all_commands(self): - """Get all commands organized by cog""" - cog_commands = {} - - for cog_name, cog in self.bot.cogs.items(): - commands_list = [] - - # Get regular commands - for cmd in cog.get_commands(): - if not cmd.hidden: - commands_list.append(cmd) - - # Get app commands (slash only) - if hasattr(cog, '__cog_app_commands__'): - for cmd in cog.__cog_app_commands__: - if isinstance(cmd, app_commands.Command): - commands_list.append(cmd) - elif isinstance(cmd, app_commands.Group): - # Add group commands - commands_list.append(cmd) - - if commands_list: - cog_commands[cog_name] = commands_list - - # Get commands not in cogs - no_cog_commands = [cmd for cmd in self.bot.commands if not cmd.cog and not cmd.hidden] - if no_cog_commands: - cog_commands["Other"] = no_cog_commands - - return cog_commands - - def search_commands(self, query): - """Search for commands matching the query""" - query = query.lower() - results = [] - - for cog_name, cog in self.bot.cogs.items(): - for cmd in cog.get_commands(): - if not cmd.hidden: - # Check command name - if query in cmd.name.lower(): - results.append((cmd, cog_name, 100)) - # Check command aliases - elif any(query in alias.lower() for alias in cmd.aliases): - results.append((cmd, cog_name, 90)) - # Check description - elif cmd.description and query in cmd.description.lower(): - results.append((cmd, cog_name, 70)) - # Check help text - elif cmd.help and query in cmd.help.lower(): - results.append((cmd, cog_name, 60)) - - # Sort by relevance - results.sort(key=lambda x: x[2], reverse=True) - return results - - def create_help_embed(self, title, description=None): - """Create a base help embed""" - embed = discord.Embed( - title=title, - description=description, - color=discord.Color.blurple() - ) - embed.set_footer(text="Use !help or /help for more info on a specific command") - return embed - - @commands.hybrid_command(name="help", description="Shows help information for commands") - @app_commands.describe(command="The command to get help for") - async def help_command(self, ctx: commands.Context, *, command: Optional[str] = None): - """ - Get help for bot commands - - Usage: - !help - Show all commands - !help - Get detailed help for a specific command - !help - Search for commands - """ - - if command: - # Try to find exact command match first - cmd = self.bot.get_command(command.lower()) - - if cmd: - # Show detailed help for specific command - embed = self.create_help_embed( - title=f"Help: {cmd.name}", - description=cmd.help or cmd.description or "No description available" - ) - - # Usage - embed.add_field( - name="Usage", - value=f"```\n{self.get_command_signature(cmd)}\n```", - inline=False - ) - - # Aliases - if cmd.aliases: - embed.add_field( - name="Aliases", - value=", ".join(f"`{alias}`" for alias in cmd.aliases), - inline=False - ) - - # Category - if cmd.cog: - embed.add_field(name="Category", value=cmd.cog.qualified_name, inline=True) - - await ctx.send(embed=embed) - else: - # Search for commands - results = self.search_commands(command) - - if not results: - # Suggest similar commands - all_cmd_names = [c.name for c in self.bot.commands] - suggestions = difflib.get_close_matches(command.lower(), all_cmd_names, n=3, cutoff=0.6) - - embed = discord.Embed( - title="❌ Command Not Found", - description=f"No command found matching `{command}`", - color=discord.Color.red() - ) - - if suggestions: - embed.add_field( - name="Did you mean?", - value="\n".join(f"• `{s}`" for s in suggestions), - inline=False - ) - - await ctx.send(embed=embed) - else: - # Show search results - embed = self.create_help_embed( - title=f"🔍 Search Results for '{command}'", - description=f"Found {len(results)} matching command(s)" - ) - - for cmd, cog_name, score in results[:10]: # Limit to 10 results - desc = cmd.description or cmd.help or "No description" - if len(desc) > 100: - desc = desc[:97] + "..." - - embed.add_field( - name=f"{cmd.name} ({cog_name})", - value=desc, - inline=False - ) - - if len(results) > 10: - embed.set_footer(text=f"Showing top 10 of {len(results)} results. Use !help for details.") - - await ctx.send(embed=embed) - else: - # Show all commands organized by category - cog_commands = self.get_all_commands() - - embed = self.create_help_embed( - title="📚 Bot Commands", - description=f"Prefix: `!` | Total Commands: {len(list(self.bot.commands))}\n\n" - "Commands work with both `!` and `/`" - ) - - for cog_name, cmds in sorted(cog_commands.items()): - if not cmds: - continue - - # Group commands by name - cmd_names = [] - seen = set() - - for cmd in cmds: - if isinstance(cmd, (commands.Command, commands.HybridCommand)): - name = cmd.name - elif isinstance(cmd, (app_commands.Command, app_commands.Group)): - name = cmd.name - else: - continue - - if name not in seen: - cmd_names.append(f"`{name}`") - seen.add(name) - - if cmd_names: - embed.add_field( - name=f"**{cog_name}**", - value=" • ".join(cmd_names), - inline=False - ) - - await ctx.send(embed=embed) - - @commands.Cog.listener() - async def on_command_error(self, ctx: commands.Context, error): - """Show help if command not found""" - if isinstance(error, commands.CommandNotFound): - # Extract the attempted command - cmd_used = ctx.message.content.split()[0][len(ctx.prefix):] - - # Suggest similar commands - all_cmd_names = [c.name for c in self.bot.commands] - suggestions = difflib.get_close_matches(cmd_used, all_cmd_names, n=3, cutoff=0.6) - - embed = discord.Embed( - title="❓ Unknown Command", - description=f"Command `{cmd_used}` not found.", - color=discord.Color.orange() - ) - - if suggestions: - embed.add_field( - name="Did you mean?", - value="\n".join(f"• `!{s}` or `/{s}`" for s in suggestions), - inline=False - ) - - embed.set_footer(text="Use !help to see all commands") - await ctx.send(embed=embed, delete_after=10) - -async def setup(bot): - # Remove default help command - bot.remove_command('help') - await bot.add_cog(Help(bot)) \ No newline at end of file diff --git a/suggestion/suggest.py b/suggestion/suggest.py index 1b18376..d03f1a3 100644 --- a/suggestion/suggest.py +++ b/suggestion/suggest.py @@ -24,6 +24,9 @@ class DenyModal(discord.ui.Modal, title="Reason for denying suggestion"): async def on_submit(self, interaction: discord.Interaction): try: + # Defer the response immediately to prevent timeout + await interaction.response.defer(ephemeral=False) + reason_text = self.reason.value or None db_path = os.path.join(os.path.dirname(__file__), "suggestions.db") @@ -31,17 +34,39 @@ 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() - await interaction.response.send_message(f"❌ Suggestion #{self.suggestion_id} denied.", ephemeral=False) + # Use followup instead of response since we deferred + await interaction.followup.send(f"❌ Suggestion #{self.suggestion_id} denied.", ephemeral=False) + # Disable buttons on the admin message + 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 as e: + print(f"Failed to edit admin message: {e}") + + # Send DM to 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 + except Exception as e: + print(f"Failed to DM user: {e}") + # Send message in original channel channel = self.bot.get_channel(self.channel_id) if channel: try: @@ -49,21 +74,16 @@ class DenyModal(discord.ui.Modal, title="Reason for denying suggestion"): if reason_text: msg += f"\n**Reason:** {reason_text}" await channel.send(msg) - except: - pass + except Exception as e: + print(f"Failed to send message in channel: {e}") - 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: - pass except Exception as e: error_msg = f"❌ Error denying suggestion: {str(e)}\n```{traceback.format_exc()}```" - await interaction.response.send_message(error_msg[:2000], ephemeral=True) + print(error_msg) + try: + await interaction.followup.send(error_msg[:2000], ephemeral=True) + except: + pass class SuggestionButtons(discord.ui.View): @@ -76,8 +96,8 @@ class SuggestionButtons(discord.ui.View): self.channel_id = channel_id self.admin_message_id = admin_message_id - approve_cid = f"suggest_approve_{suggestion_id}" - deny_cid = f"suggest_deny_{suggestion_id}" + approve_cid = f"suggest_approve_{suggestion_id}" if suggestion_id else "suggest_approve" + deny_cid = f"suggest_deny_{suggestion_id}" if suggestion_id else "suggest_deny" approve_btn = discord.ui.Button(label="Approve ✅", style=discord.ButtonStyle.success, custom_id=approve_cid, disabled=disabled) approve_btn.callback = self.approve @@ -97,43 +117,61 @@ class SuggestionButtons(discord.ui.View): await interaction.response.send_message("⚠️ This button is no longer active.", ephemeral=True) return + # Defer immediately to prevent timeout + await interaction.response.defer(ephemeral=False) + 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!**") + # Use followup since we deferred + await interaction.followup.send(f"✅ Suggestion #{self.suggestion_id} approved.", ephemeral=False) + # Disable buttons on the admin message 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) + 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: - for item in self.children: - item.disabled = True - try: - await interaction.message.edit(view=self) - except: - pass + except Exception as e: + print(f"Failed to edit admin message: {e}") + + # Send DM to 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 Exception as e: + print(f"Failed to DM user: {e}") + + # Send message in original channel + channel = self.bot.get_channel(self.channel_id) + if channel: + try: + await channel.send(f"✅ Suggestion **#{self.suggestion_id}** (`{self.suggestion_text}`) has been **approved!**") + except Exception as e: + print(f"Failed to send message in channel: {e}") + except Exception as e: error_msg = f"❌ Error approving suggestion: {str(e)}\n```{traceback.format_exc()}```" + print(error_msg) try: - await interaction.response.send_message(error_msg[:2000], ephemeral=True) + if not interaction.response.is_done(): + await interaction.response.send_message(error_msg[:2000], ephemeral=True) + else: + await interaction.followup.send(error_msg[:2000], ephemeral=True) except: - await interaction.followup.send(error_msg[:2000], ephemeral=True) + pass async def deny(self, interaction: discord.Interaction): try: @@ -145,14 +183,25 @@ class SuggestionButtons(discord.ui.View): await interaction.response.send_message("⚠️ This button is no longer active.", ephemeral=True) return - 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) + 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) except Exception as e: error_msg = f"❌ Error opening deny modal: {str(e)}\n```{traceback.format_exc()}```" + print(error_msg) try: - await interaction.response.send_message(error_msg[:2000], ephemeral=True) + if not interaction.response.is_done(): + await interaction.response.send_message(error_msg[:2000], ephemeral=True) + else: + await interaction.followup.send(error_msg[:2000], ephemeral=True) except: - await interaction.followup.send(error_msg[:2000], ephemeral=True) + pass class PaginationView(discord.ui.View): @@ -218,16 +267,17 @@ class Suggestion(commands.Cog): rows = await cursor.fetchall() for sid, uid, suggestion_text, channel_id, admin_msg_id in rows: - view = SuggestionButtons( - self.bot, - suggestion_id=sid, - user_id=uid, - suggestion_text=suggestion_text, - channel_id=channel_id, - admin_message_id=admin_msg_id - ) - self.bot.add_view(view, message_id=admin_msg_id) - print(f"Re-registered view for suggestion #{sid} (message {admin_msg_id})") + if admin_msg_id: # Only register if we have a message ID + view = SuggestionButtons( + self.bot, + suggestion_id=sid, + user_id=uid, + suggestion_text=suggestion_text, + channel_id=channel_id, + admin_message_id=admin_msg_id + ) + self.bot.add_view(view, message_id=admin_msg_id) + print(f"Re-registered view for suggestion #{sid} (message {admin_msg_id})") async def cog_unload(self): if self.db: @@ -280,6 +330,7 @@ class Suggestion(commands.Cog): except Exception as e: print(f"Failed to send DM to admin: {e}") + traceback.print_exc() except Exception as e: error_msg = f"❌ An error occurred: {str(e)}\n```{traceback.format_exc()}```" diff --git a/xp/backup_xp.py b/xp/backup_xp.py index e8daf15..f877d78 100644 --- a/xp/backup_xp.py +++ b/xp/backup_xp.py @@ -145,25 +145,8 @@ class BackupXP(commands.Cog): annual_size = os.path.getsize(annual_db) / (1024 * 1024) total_size = lifetime_size + annual_size - backup_type = "Auto Backup" if is_auto else "Manual Backup" - msg = ( - f"✅ **{backup_type}** - Databases backed up successfully!\n" - f"**Lifetime:** `{os.path.basename(lifetime_backup)}` ({lifetime_size:.2f} MB)\n" - f"**Annual:** `{os.path.basename(annual_backup)}` ({annual_size:.2f} MB)\n" - f"**Total size:** {total_size:.2f} MB" - ) - - if log_channel: - # Send to notification channel only - notification_channel = self.bot.get_channel(NOTIFICATION_CHANNEL_ID) - if notification_channel: - print(f"[Backup] Sending notification to channel {NOTIFICATION_CHANNEL_ID}") - await notification_channel.send(msg) - else: - print(f"[Backup] Could not find notification channel {NOTIFICATION_CHANNEL_ID}") - print(f"[Backup] Backup completed successfully") - return True, msg + return True except Exception as e: error_msg = f"❌ Backup failed: `{e}`" diff --git a/xp/reset_task.py b/xp/reset_task.py index a420ba6..2c823a9 100644 --- a/xp/reset_task.py +++ b/xp/reset_task.py @@ -45,18 +45,6 @@ class ResetTask(commands.Cog): if last_reset_month != now.month: reset_leaderboard("monthly") print(f"[XP System] Monthly leaderboard reset at {now}") - await self.send_reset_notification(notification_channel_id, user_id, "Monthly") - - async def send_reset_notification(self, channel_id, user_id, reset_type): - """Send a notification message when a reset occurs.""" - try: - channel = self.bot.get_channel(channel_id) - if channel: - await channel.send(f"<@{user_id}> {reset_type} leaderboard has been reset! 🔄") - else: - print(f"[XP System] Could not find channel {channel_id} for reset notification") - except Exception as e: - print(f"[XP System] Error sending reset notification: {e}") def should_reset_daily(self, now): """Check if it's time for daily reset (00:00 UTC)."""