diff --git a/bot.py b/bot.py index 93f23eb..7444dae 100644 --- a/bot.py +++ b/bot.py @@ -2587,6 +2587,122 @@ def check_moderation_permission(user_permission): """Checks if the user has moderation rights (Permission 5 or higher)""" return user_permission >= 5 +async def log_moderation_action(guild, action_type, moderator, target_user, reason, duration=None, additional_info=None): + """Logs moderation actions to the configured log channel""" + try: + guild_settings = get_guild_settings(guild.id) + + # Check if logging is enabled and channel is configured + if not guild_settings["mod_log_enabled"] or not guild_settings["log_channel_id"]: + return + + log_channel = guild.get_channel(guild_settings["log_channel_id"]) + if not log_channel: + logger.warning(f"Log channel {guild_settings['log_channel_id']} not found in guild {guild.id}") + return + + # Create log embed + color_map = { + "warn": 0xff9500, + "mute": 0xff0000, + "unmute": 0x00ff00, + "kick": 0xff6600, + "ban": 0x8b0000, + "unban": 0x00ff00 + } + + embed = discord.Embed( + title=f"🛡️ Moderation Action: {action_type.title()}", + color=color_map.get(action_type.lower(), 0x3498db), + timestamp=datetime.now() + ) + + embed.add_field(name="👤 Target User", value=f"{target_user.mention}\n`{target_user.id}`", inline=True) + embed.add_field(name="👮 Moderator", value=f"{moderator.mention}\n`{moderator.id}`", inline=True) + embed.add_field(name="📝 Reason", value=reason, inline=True) + + if duration: + embed.add_field(name="⏱️ Duration", value=duration, inline=True) + + if additional_info: + for key, value in additional_info.items(): + embed.add_field(name=key, value=value, inline=True) + + embed.set_thumbnail(url=target_user.display_avatar.url) + embed.set_footer(text=f"Action ID: {guild.id}-{target_user.id}-{int(datetime.now().timestamp())}") + + await log_channel.send(embed=embed) + logger.info(f"Logged {action_type} action for user {target_user.id} in guild {guild.id}") + + except Exception as e: + logger.error(f"Error logging moderation action: {e}") + +async def save_warning_to_database(user_id, guild_id, moderator_id, reason, timestamp=None): + """Saves individual warning records to the database""" + connection = None + cursor = None + try: + connection = connect_to_database() + cursor = connection.cursor() + + if timestamp is None: + timestamp = datetime.now() + + insert_query = """ + INSERT INTO user_warnings (user_id, guild_id, moderator_id, reason, created_at) + VALUES (%s, %s, %s, %s, %s) + """ + + cursor.execute(insert_query, (user_id, guild_id, moderator_id, reason, timestamp)) + connection.commit() + + logger.info(f"Saved warning record for user {user_id} in guild {guild_id}") + return cursor.lastrowid + + except Exception as e: + logger.error(f"Error saving warning to database: {e}") + if connection: + connection.rollback() + return None + finally: + if cursor: + cursor.close() + if connection: + close_database_connection(connection) + +def create_warnings_table(): + """Creates the user_warnings table if it doesn't exist""" + connection = None + cursor = None + try: + connection = connect_to_database() + cursor = connection.cursor() + + create_table_query = """ + CREATE TABLE IF NOT EXISTS user_warnings ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + guild_id BIGINT NOT NULL, + moderator_id BIGINT NOT NULL, + reason TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_user_guild (user_id, guild_id), + INDEX idx_created_at (created_at) + ) + """ + + cursor.execute(create_table_query) + connection.commit() + logger.info("User warnings table checked/created successfully") + + except Exception as e: + logger.error(f"Error creating warnings table: {e}") + finally: + if cursor: + cursor.close() + if connection: + close_database_connection(connection) + async def save_user_roles(user_id, guild_id, roles): """Saves a user's roles before a mute""" connection = None @@ -2669,46 +2785,157 @@ async def warn(ctx, user: discord.User, *, reason: str = "No reason provided"): # Check moderation rights if not check_moderation_permission(mod_data["permission"]): - await ctx.send("❌ You don't have permission to use this command. (Requires Permission Level 5 or higher)") + embed = discord.Embed( + title="❌ Insufficient Permissions", + description="You need moderation permissions (Level 5 or higher) to use this command.", + color=0xff0000 + ) + await ctx.send(embed=embed, ephemeral=True) + return + + # Cannot warn yourself + if user.id == ctx.author.id: + embed = discord.Embed( + title="❌ Invalid Action", + description="You cannot warn yourself!", + color=0xff0000 + ) + await ctx.send(embed=embed, ephemeral=True) + return + + # Check if target has higher permissions + target_data = await load_user_data(user.id, ctx.guild.id) + + if target_data["permission"] >= mod_data["permission"]: + embed = discord.Embed( + title="❌ Insufficient Permissions", + description="You cannot warn someone with equal or higher permissions than you.", + color=0xff0000 + ) + await ctx.send(embed=embed, ephemeral=True) return - # Load user data - user_data = await load_user_data(user.id, ctx.guild.id) - # Increase warn count - user_data["warns"] += 1 - update_user_data(user.id, ctx.guild.id, "warns", user_data["warns"]) + target_data["warns"] += 1 + update_user_data(user.id, ctx.guild.id, "warns", target_data["warns"]) + + # Save detailed warning record to database + warning_id = await save_warning_to_database( + user_id=user.id, + guild_id=ctx.guild.id, + moderator_id=ctx.author.id, + reason=reason + ) + + # Get guild settings for threshold checking + guild_settings = get_guild_settings(ctx.guild.id) + warn_threshold = guild_settings.get("max_warn_threshold", 3) # Create embed embed = discord.Embed( - title="⚠️ Warning issued", + title="⚠️ Warning Issued", description=f"{user.mention} has been warned.", color=0xff9500, timestamp=datetime.now() ) - embed.add_field(name="Reason", value=reason, inline=False) - embed.add_field(name="Moderator", value=ctx.author.mention, inline=True) - embed.add_field(name="Warning Count", value=f"{user_data['warns']}", inline=True) + embed.add_field(name="📝 Reason", value=reason, inline=False) + embed.add_field(name="👮 Moderator", value=ctx.author.mention, inline=True) + embed.add_field(name="⚠️ Warning Count", value=f"{target_data['warns']}/{warn_threshold}", inline=True) + + if warning_id: + embed.add_field(name="🆔 Warning ID", value=str(warning_id), inline=True) + embed.set_footer(text=f"User ID: {user.id}") + embed.set_thumbnail(url=user.display_avatar.url) + + # Check if user has reached the warning threshold + if target_data['warns'] >= warn_threshold: + auto_mute_enabled = guild_settings.get("auto_mute_on_warns", False) + if auto_mute_enabled: + mute_role_id = guild_settings.get("mute_role_id") + if mute_role_id and ctx.guild.get_role(mute_role_id): + mute_role = ctx.guild.get_role(mute_role_id) + try: + member = ctx.guild.get_member(user.id) + if member: + await member.add_roles(mute_role, reason=f"Automatic mute: {warn_threshold} warnings reached") + embed.add_field( + name="🔇 Automatic Action", + value=f"User has been automatically muted for reaching {warn_threshold} warnings.", + inline=False + ) + + # Log the automatic mute action + await log_moderation_action( + guild=ctx.guild, + action_type="mute", + moderator=ctx.author, + target_user=user, + reason=f"Automatic mute: {warn_threshold} warnings reached", + additional_info={"Trigger": f"Warning #{target_data['warns']}"} + ) + except discord.Forbidden: + embed.add_field( + name="⚠️ Warning", + value="Could not automatically mute user due to insufficient permissions.", + inline=False + ) + else: + embed.add_field( + name="🚨 Threshold Reached", + value=f"User has reached the warning threshold ({warn_threshold} warnings). Consider further action.", + inline=False + ) await ctx.send(embed=embed) + + # Log the warning action + await log_moderation_action( + guild=ctx.guild, + action_type="warn", + moderator=ctx.author, + target_user=user, + reason=reason, + additional_info={ + "Warning Count": f"{target_data['warns']}/{warn_threshold}", + "Warning ID": str(warning_id) if warning_id else "N/A" + } + ) + + # Try to DM the user + try: + dm_embed = discord.Embed( + title=f"⚠️ Warning from {ctx.guild.name}", + color=0xff9500, + timestamp=datetime.now() + ) + dm_embed.add_field(name="👮 Moderator", value=ctx.author.display_name, inline=True) + dm_embed.add_field(name="📝 Reason", value=reason, inline=False) + dm_embed.add_field(name="⚠️ Total Warnings", value=f"{target_data['warns']}/{warn_threshold}", inline=True) + + if target_data['warns'] >= warn_threshold: + dm_embed.add_field( + name="🔇 Additional Action", + value="You have reached the warning threshold. Further violations may result in more severe punishments.", + inline=False + ) + + await user.send(embed=dm_embed) + except discord.Forbidden: + # User has DMs disabled + pass # Log the action logger.info(f"User {user.id} warned by {ctx.author.id} in guild {ctx.guild.id}. Reason: {reason}") - # Auto-actions based on warning count - if user_data["warns"] >= 3: - embed_auto = discord.Embed( - title="🚨 Auto-action triggered", - description=f"{user.mention} has reached {user_data['warns']} warnings!", - color=0xff0000 - ) - embed_auto.add_field(name="Recommendation", value="Consider further moderation measures", inline=False) - await ctx.send(embed=embed_auto) - except Exception as e: logger.error(f"Error in warn command: {e}") - await ctx.send("❌ An error occurred while warning the user.") + embed = discord.Embed( + title="❌ Error", + description="An error occurred while processing the warning. Please try again.", + color=0xff0000 + ) + await ctx.send(embed=embed) @client.hybrid_command() async def mywarns(ctx): @@ -2723,11 +2950,18 @@ async def mywarns(ctx): timestamp=datetime.now() ) - # Warning information - warn_color = "🟢" if user_data["warns"] == 0 else "🟡" if user_data["warns"] < 3 else "🔴" + # Get guild settings for thresholds + guild_settings = get_guild_settings(ctx.guild.id) + + # Get detailed warning records + warning_records = await get_user_warnings(ctx.author.id, ctx.guild.id) + + # Warning information with threshold + warn_threshold = guild_settings.get("max_warn_threshold", 3) + warn_color = "🟢" if user_data["warns"] == 0 else "🟡" if user_data["warns"] < warn_threshold else "🔴" embed.add_field( name=f"{warn_color} Warnings", - value=f"**{user_data['warns']}** warning(s)", + value=f"**{user_data['warns']}/{warn_threshold}** warnings", inline=True ) @@ -2765,11 +2999,37 @@ async def mywarns(ctx): embed.add_field(name="📈 Status", value=status, inline=False) embed.color = status_color + # Detailed warning history (show last 5 warnings) + if warning_records: + warning_history = "**Recent Warning History:**\n" + display_count = min(5, len(warning_records)) + + for i in range(display_count): + record = warning_records[i] + moderator = ctx.guild.get_member(record["moderator_id"]) + mod_name = moderator.display_name if moderator else f"ID: {record['moderator_id']}" + + # Format date + warning_date = record["created_at"].strftime("%d.%m.%Y %H:%M") + + # Truncate reason if too long + reason = record["reason"] + if len(reason) > 50: + reason = reason[:47] + "..." + + warning_history += f"`{warning_date}` - **{mod_name}**: {reason}\n" + + if len(warning_records) > 5: + warning_history += f"\n*... and {len(warning_records) - 5} more warning(s)*" + + embed.add_field(name="📋 Warning Details", value=warning_history, inline=False) + # Get guild settings for thresholds - guild_settings = get_guild_settings(ctx.guild.id) - threshold_info = f"Warning threshold: **{guild_settings['max_warn_threshold']}** warnings" - if guild_settings["auto_mute_on_warns"]: - threshold_info += f"\nAuto-mute duration: **{guild_settings['auto_mute_duration']}**" + threshold_info = f"Warning threshold: **{warn_threshold}** warnings" + auto_mute_enabled = guild_settings.get("auto_mute_on_warns", False) + if auto_mute_enabled: + auto_mute_duration = guild_settings.get("auto_mute_duration", "1 hour") + threshold_info += f"\nAuto-mute: **{auto_mute_duration}** (at {warn_threshold} warnings)" embed.add_field(name="⚙️ Server Settings", value=threshold_info, inline=False) @@ -3412,6 +3672,10 @@ async def delnotes(ctx): await ctx.send(f"No notes found for user {ctx.author.name}.") try: + # Initialize database tables + create_warnings_table() + logger.info("Database tables initialized successfully") + loop.run_until_complete(client.start(TOKEN)) except KeyboardInterrupt: loop.run_until_complete(client.logout())