diff --git a/bot.py b/bot.py index 7172002..eaff55e 100644 --- a/bot.py +++ b/bot.py @@ -321,7 +321,7 @@ def load_user_data_from_mysql(user_id, guild_id): } # Count warnings from user_warnings table - warning_count_query = "SELECT COUNT(*) FROM user_warnings WHERE user_id = %s AND guild_id = %s" + warning_count_query = "SELECT COUNT(*) FROM user_warnings WHERE user_id = %s AND guild_id = %s AND aktiv = TRUE" cursor.execute(warning_count_query, (user_id, guild_id)) warning_count = cursor.fetchone()[0] user_data["warns"] = warning_count @@ -2106,6 +2106,8 @@ async def modhelp(ctx): "`/unmute ` - Manually unmute a user\n" "`/modinfo [user]` - View comprehensive user information\n" "`/viewwarn ` - View detailed warning information\n" + "`/removewarn ` - Deactivate a warning (Level 6+)\n" + "`/restorewarn ` - Reactivate a warning (Level 6+)\n" "`/modstats [user]` - View moderation statistics\n" "`/processes [type]` - Manage active processes\n" "`/startgiveaway` - Create server giveaways\n" @@ -2793,14 +2795,14 @@ async def save_warning_to_database(user_id, guild_id, moderator_id, reason, time insert_query = """ INSERT INTO user_warnings (user_id, guild_id, moderator_id, reason, message_id, message_content, message_attachments, message_author_id, - message_channel_id, context_messages, created_at) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + message_channel_id, context_messages, aktiv, created_at) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ cursor.execute(insert_query, ( user_id, guild_id, moderator_id, reason, message_id_db, message_content, message_attachments, message_author_id, - message_channel_id, context_messages, timestamp + message_channel_id, context_messages, True, timestamp )) connection.commit() @@ -2839,10 +2841,12 @@ def create_warnings_table(): message_author_id BIGINT NULL, message_channel_id BIGINT NULL, context_messages LONGTEXT NULL, + aktiv BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_guild (user_id, guild_id), INDEX idx_created_at (created_at), - INDEX idx_message_id (message_id) + INDEX idx_message_id (message_id), + INDEX idx_aktiv (aktiv) ) """ @@ -2856,7 +2860,9 @@ def create_warnings_table(): "ALTER TABLE user_warnings ADD COLUMN message_author_id BIGINT NULL", "ALTER TABLE user_warnings ADD COLUMN message_channel_id BIGINT NULL", "ALTER TABLE user_warnings ADD COLUMN context_messages LONGTEXT NULL", - "ALTER TABLE user_warnings ADD INDEX idx_message_id (message_id)" + "ALTER TABLE user_warnings ADD COLUMN aktiv BOOLEAN DEFAULT TRUE", + "ALTER TABLE user_warnings ADD INDEX idx_message_id (message_id)", + "ALTER TABLE user_warnings ADD INDEX idx_aktiv (aktiv)" ] for alter_query in alter_queries: @@ -2877,8 +2883,14 @@ def create_warnings_table(): if connection: close_database_connection(connection) -async def get_user_warnings(user_id, guild_id): - """Retrieves all warning records for a user""" +async def get_user_warnings(user_id, guild_id, active_only=True): + """Retrieves warning records for a user + + Args: + user_id: Discord user ID + guild_id: Discord guild ID + active_only: If True, only returns active warnings. If False, returns all warnings. + """ connection = None cursor = None try: @@ -2887,11 +2899,11 @@ async def get_user_warnings(user_id, guild_id): select_query = """ SELECT id, moderator_id, reason, created_at, message_id, message_content, - message_attachments, message_author_id, message_channel_id, context_messages + message_attachments, message_author_id, message_channel_id, context_messages, aktiv FROM user_warnings - WHERE user_id = %s AND guild_id = %s + WHERE user_id = %s AND guild_id = %s {} ORDER BY created_at DESC - """ + """.format("AND aktiv = TRUE" if active_only else "") cursor.execute(select_query, (user_id, guild_id)) results = cursor.fetchall() @@ -2908,7 +2920,8 @@ async def get_user_warnings(user_id, guild_id): "message_attachments": row[6], "message_author_id": row[7], "message_channel_id": row[8], - "context_messages": row[9] + "context_messages": row[9], + "aktiv": row[10] }) return warnings @@ -2922,6 +2935,66 @@ async def get_user_warnings(user_id, guild_id): if connection: close_database_connection(connection) +async def deactivate_warning(warning_id): + """Deactivates a warning by setting aktiv to FALSE""" + connection = None + cursor = None + try: + connection = connect_to_database() + cursor = connection.cursor() + + update_query = "UPDATE user_warnings SET aktiv = FALSE WHERE id = %s" + cursor.execute(update_query, (warning_id,)) + connection.commit() + + if cursor.rowcount > 0: + logger.info(f"Deactivated warning with ID {warning_id}") + return True + else: + logger.warning(f"No warning found with ID {warning_id}") + return False + + except Exception as e: + logger.error(f"Error deactivating warning: {e}") + if connection: + connection.rollback() + return False + finally: + if cursor: + cursor.close() + if connection: + close_database_connection(connection) + +async def reactivate_warning(warning_id): + """Reactivates a warning by setting aktiv to TRUE""" + connection = None + cursor = None + try: + connection = connect_to_database() + cursor = connection.cursor() + + update_query = "UPDATE user_warnings SET aktiv = TRUE WHERE id = %s" + cursor.execute(update_query, (warning_id,)) + connection.commit() + + if cursor.rowcount > 0: + logger.info(f"Reactivated warning with ID {warning_id}") + return True + else: + logger.warning(f"No warning found with ID {warning_id}") + return False + + except Exception as e: + logger.error(f"Error reactivating warning: {e}") + if connection: + connection.rollback() + return False + finally: + if cursor: + cursor.close() + if connection: + close_database_connection(connection) + async def get_message_data(channel, message_id, context_range=3): """Retrieves and processes message data for warning documentation with context messages""" try: @@ -3509,8 +3582,8 @@ async def account(ctx, user: discord.User = None): # Get guild settings for thresholds guild_settings = get_guild_settings(ctx.guild.id) - # Get detailed warning records - warning_records = await get_user_warnings(target_user.id, ctx.guild.id) + # Get detailed warning records (active warnings only for user view) + warning_records = await get_user_warnings(target_user.id, ctx.guild.id, active_only=True) # Warning information with threshold warn_threshold = guild_settings.get("max_warn_threshold", 3) @@ -3680,8 +3753,8 @@ async def modinfo(ctx, user: discord.User = None): guild_settings = get_guild_settings(ctx.guild.id) warn_threshold = guild_settings.get("max_warn_threshold", 3) - # Get detailed warning records - warning_records = await get_user_warnings(user.id, ctx.guild.id) + # Get detailed warning records (all warnings for moderator view) + warning_records = await get_user_warnings(user.id, ctx.guild.id, active_only=False) # Create main embed embed = discord.Embed( @@ -3778,7 +3851,9 @@ async def modinfo(ctx, user: discord.User = None): if len(reason) > 50: reason = reason[:47] + "..." - warning_line = f"`{warning_date}` **{mod_name}**: {reason}" + # Add status indicator + status_indicator = "🟢" if record.get("aktiv", True) else "šŸ”“" + warning_line = f"`{warning_date}` {status_indicator} **{mod_name}**: {reason}" # Add message indicator and content preview if available if record.get("message_id"): @@ -3792,6 +3867,8 @@ async def modinfo(ctx, user: discord.User = None): if len(warning_records) > 3: warning_history += f"*... and {len(warning_records) - 3} more warning(s)*" + warning_history += f"\n🟢 = Active Warning | šŸ”“ = Deactivated Warning" + embed.add_field(name="šŸ“‹ Recent Warnings", value=warning_history, inline=False) # Server activity (if member) @@ -3899,7 +3976,7 @@ async def viewwarn(ctx, warning_id: int): select_query = """ SELECT user_id, guild_id, moderator_id, reason, created_at, message_id, - message_content, message_attachments, message_author_id, message_channel_id, context_messages + message_content, message_attachments, message_author_id, message_channel_id, context_messages, aktiv FROM user_warnings WHERE id = %s AND guild_id = %s """ @@ -3917,7 +3994,7 @@ async def viewwarn(ctx, warning_id: int): return # Parse result - user_id, guild_id, moderator_id, reason, created_at, message_id, message_content, message_attachments, message_author_id, message_channel_id, context_messages = result + user_id, guild_id, moderator_id, reason, created_at, message_id, message_content, message_attachments, message_author_id, message_channel_id, context_messages, aktiv = result # Get user and moderator objects warned_user = await client.fetch_user(user_id) @@ -3933,6 +4010,13 @@ async def viewwarn(ctx, warning_id: int): embed.add_field(name="šŸ‘¤ Warned User", value=f"{warned_user.mention}\n`{warned_user.id}`", inline=True) embed.add_field(name="šŸ‘® Moderator", value=f"{moderator.mention}\n`{moderator.id}`", inline=True) embed.add_field(name="šŸ“… Date", value=f"", inline=True) + + # Add status field + status_text = "🟢 **Active**" if aktiv else "šŸ”“ **Deactivated**" + embed.add_field(name="šŸ“Š Status", value=status_text, inline=True) + embed.add_field(name="šŸ†” Warning ID", value=f"`{warning_id}`", inline=True) + embed.add_field(name="", value="", inline=True) # Empty field for spacing + embed.add_field(name="šŸ“ Reason", value=reason, inline=False) # Add message information if available @@ -4046,6 +4130,238 @@ async def viewwarn(ctx, warning_id: int): ) await send_response(embed=embed) +@client.hybrid_command() +async def removewarn(ctx, warning_id: int): + """Deactivates a warning (makes it hidden from /account but keeps data) (Requires Permission Level 6 or higher) + + Usage: /removewarn 123 + """ + # Check if it's a slash command and defer if needed + is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction + if is_slash_command: + await ctx.defer() + + # Helper function for sending responses + async def send_response(content=None, embed=None, ephemeral=False, file=None): + try: + if is_slash_command: + await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file) + else: + await ctx.send(content=content, embed=embed, file=file) + except Exception as e: + logger.error(f"Error sending response: {e}") + try: + await ctx.send(content=content, embed=embed, file=file) + except: + pass + + try: + # Check permissions + user_data = await load_user_data(ctx.author.id, ctx.guild.id) + if user_data["permission"] < 6: + embed = discord.Embed( + title="āŒ Insufficient Permissions", + description="You need Level 6 or higher permissions to deactivate warnings.", + color=0xff0000 + ) + await send_response(embed=embed, ephemeral=True) + return + + # Get warning info first + connection = connect_to_database() + cursor = connection.cursor() + + cursor.execute("SELECT user_id, guild_id, reason, aktiv FROM user_warnings WHERE id = %s", (warning_id,)) + warning_data = cursor.fetchone() + + if not warning_data: + embed = discord.Embed( + title="āŒ Warning Not Found", + description=f"No warning found with ID `{warning_id}`.", + color=0xff0000 + ) + await send_response(embed=embed, ephemeral=True) + return + + if warning_data[1] != ctx.guild.id: + embed = discord.Embed( + title="āŒ Invalid Warning", + description="This warning doesn't belong to this server.", + color=0xff0000 + ) + await send_response(embed=embed, ephemeral=True) + return + + if not warning_data[3]: # Already inactive + embed = discord.Embed( + title="āš ļø Warning Already Inactive", + description=f"Warning `{warning_id}` is already deactivated.", + color=0xffa500 + ) + await send_response(embed=embed, ephemeral=True) + return + + # Deactivate the warning + success = await deactivate_warning(warning_id) + + if success: + try: + target_user = await client.fetch_user(warning_data[0]) + user_name = target_user.display_name + except: + user_name = f"User {warning_data[0]}" + + embed = discord.Embed( + title="āœ… Warning Deactivated", + description=f"Warning `{warning_id}` for {user_name} has been deactivated.\n\n**Reason:** {warning_data[2]}\n\nThe warning is now hidden from `/account` but data is preserved for admin review.", + color=0x00ff00 + ) + await send_response(embed=embed) + + # Log the action + await log_moderation_action( + ctx.guild.id, + f"Warning `{warning_id}` deactivated by {ctx.author.display_name}", + ctx.author + ) + else: + embed = discord.Embed( + title="āŒ Error", + description="Failed to deactivate warning. Please try again.", + color=0xff0000 + ) + await send_response(embed=embed) + + except Exception as e: + logger.error(f"Error in removewarn command: {e}") + embed = discord.Embed( + title="āŒ Error", + description="An error occurred while deactivating the warning.", + color=0xff0000 + ) + await send_response(embed=embed) + finally: + if cursor: + cursor.close() + if connection: + close_database_connection(connection) + +@client.hybrid_command() +async def restorewarn(ctx, warning_id: int): + """Reactivates a previously deactivated warning (Requires Permission Level 6 or higher) + + Usage: /restorewarn 123 + """ + # Check if it's a slash command and defer if needed + is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction + if is_slash_command: + await ctx.defer() + + # Helper function for sending responses + async def send_response(content=None, embed=None, ephemeral=False, file=None): + try: + if is_slash_command: + await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file) + else: + await ctx.send(content=content, embed=embed, file=file) + except Exception as e: + logger.error(f"Error sending response: {e}") + try: + await ctx.send(content=content, embed=embed, file=file) + except: + pass + + try: + # Check permissions + user_data = await load_user_data(ctx.author.id, ctx.guild.id) + if user_data["permission"] < 6: + embed = discord.Embed( + title="āŒ Insufficient Permissions", + description="You need Level 6 or higher permissions to reactivate warnings.", + color=0xff0000 + ) + await send_response(embed=embed, ephemeral=True) + return + + # Get warning info first + connection = connect_to_database() + cursor = connection.cursor() + + cursor.execute("SELECT user_id, guild_id, reason, aktiv FROM user_warnings WHERE id = %s", (warning_id,)) + warning_data = cursor.fetchone() + + if not warning_data: + embed = discord.Embed( + title="āŒ Warning Not Found", + description=f"No warning found with ID `{warning_id}`.", + color=0xff0000 + ) + await send_response(embed=embed, ephemeral=True) + return + + if warning_data[1] != ctx.guild.id: + embed = discord.Embed( + title="āŒ Invalid Warning", + description="This warning doesn't belong to this server.", + color=0xff0000 + ) + await send_response(embed=embed, ephemeral=True) + return + + if warning_data[3]: # Already active + embed = discord.Embed( + title="āš ļø Warning Already Active", + description=f"Warning `{warning_id}` is already active.", + color=0xffa500 + ) + await send_response(embed=embed, ephemeral=True) + return + + # Reactivate the warning + success = await reactivate_warning(warning_id) + + if success: + try: + target_user = await client.fetch_user(warning_data[0]) + user_name = target_user.display_name + except: + user_name = f"User {warning_data[0]}" + + embed = discord.Embed( + title="āœ… Warning Reactivated", + description=f"Warning `{warning_id}` for {user_name} has been reactivated.\n\n**Reason:** {warning_data[2]}\n\nThe warning is now visible in `/account` again.", + color=0x00ff00 + ) + await send_response(embed=embed) + + # Log the action + await log_moderation_action( + ctx.guild.id, + f"Warning `{warning_id}` reactivated by {ctx.author.display_name}", + ctx.author + ) + else: + embed = discord.Embed( + title="āŒ Error", + description="Failed to reactivate warning. Please try again.", + color=0xff0000 + ) + await send_response(embed=embed) + + except Exception as e: + logger.error(f"Error in restorewarn command: {e}") + embed = discord.Embed( + title="āŒ Error", + description="An error occurred while reactivating the warning.", + color=0xff0000 + ) + await send_response(embed=embed) + finally: + if cursor: + cursor.close() + if connection: + close_database_connection(connection) + @client.hybrid_command() async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason provided", message_id: str = None, context_range: int = 3): """Mutes a user for a specified duration (Requires Permission Level 5 or higher)