diff --git a/bot.py b/bot.py index e10b136..cc82b7c 100644 --- a/bot.py +++ b/bot.py @@ -2874,11 +2874,12 @@ async def on_message(message): guild_id = message.guild.id member = message.author # Das Member-Objekt für Datenaktualisierung - # ── Honeypot check ────────────────────────────────────────────────────────── + # ── Honeypot check ───────────────────────────────────────────────────────────── + guild_id = message.guild.id guild_settings = get_guild_settings(guild_id) + if guild_settings.get("honeypot_enabled") and guild_settings.get("honeypot_channel_id"): if message.channel.id == int(guild_settings["honeypot_channel_id"]): - # Build ignore-role set ignore_role_ids = set() raw_ignore = guild_settings.get("honeypot_ignore_roles") if raw_ignore: @@ -2887,9 +2888,12 @@ async def on_message(message): except Exception: pass + member = message.author if isinstance(message.author, discord.Member) else message.guild.get_member(message.author.id) + if member is None or member.bot: + return + member_role_ids = {role.id for role in member.roles} if not (ignore_role_ids & member_role_ids): - # Delete the honeypot message try: await message.delete() except Exception: @@ -2899,67 +2903,36 @@ async def on_message(message): acc_age_min = int(guild_settings.get("honeypot_acc_age_min") or 30) preserve = guild_settings.get("honeypot_preserve_old_accounts", False) - # Determine: old-account mute vs ban if preserve and member.joined_at: now_aware = datetime.now(member.joined_at.tzinfo) days_on_server = (now_aware - member.joined_at).days is_old_account = days_on_server >= acc_age_min else: + days_on_server = None is_old_account = False if is_old_account: - # Apply 1-year (365 days) mute via the mute system - honeypot_role = None - hp_role_id = guild_settings.get("honeypot_get_role") - if hp_role_id: - try: - honeypot_role = message.guild.get_role(int(hp_role_id)) - except Exception: - pass - if honeypot_role is None: - honeypot_role = await get_or_create_mute_role(message.guild, guild_settings) - - if honeypot_role: - try: - await member.add_roles( - honeypot_role, - reason="Honeypot: wrote in honeypot channel (account protected as old member)" - ) - # Persist mute record for 365 days - start_time = datetime.now() - end_time = start_time + timedelta(days=365) - process_data = { - "user_id": member.id, - "guild_id": guild_id, - "channel_id": message.channel.id, - "reason": "Honeypot trigger", - "moderator_id": client.user.id, - "mute_role_id": honeypot_role.id - } - process_uuid = create_active_process( - process_type="mute", - guild_id=guild_id, - channel_id=message.channel.id, - user_id=member.id, - target_id=member.id, - end_time=end_time, - data=process_data - ) - await save_mute_to_database( - user_id=member.id, - guild_id=guild_id, - moderator_id=client.user.id, - reason="Honeypot: wrote in honeypot channel (account protected as old member)", - duration="365d", - start_time=start_time, - end_time=end_time, - process_uuid=process_uuid, - channel_id=message.channel.id, - mute_role_id=honeypot_role.id - ) - action_taken = "mute" - except discord.Forbidden: - logger.warning(f"Honeypot: no permission to mute {member.id} in guild {guild_id}") + try: + result = await apply_mute_action( + guild=message.guild, + member=member, + moderator=client.user, + duration_seconds=365 * 86400, + duration_label="365d", + reason="Honeypot: wrote in honeypot channel (protected old member)", + source_channel=message.channel, + message_data=None, + message_id=message.id, + remove_existing_roles=False, + save_removed_roles=False, + increment_mute_count=True + ) + action_taken = "mute" + honeypot_mute_id = result["mute_id"] + except discord.Forbidden: + logger.warning(f"Honeypot: no permission to mute {member.id} in guild {guild_id}") + except Exception as e: + logger.error(f"Honeypot mute failed for {member.id} in guild {guild_id}: {e}") else: try: await message.guild.ban( @@ -2970,15 +2943,17 @@ async def on_message(message): action_taken = "ban" except discord.Forbidden: logger.warning(f"Honeypot: no permission to ban {member.id} in guild {guild_id}") + except Exception as e: + logger.error(f"Honeypot ban failed for {member.id} in guild {guild_id}: {e}") - # Send log embed hp_log_ch_id = guild_settings.get("honeypot_log_channel_id") if action_taken and hp_log_ch_id: try: log_ch = message.guild.get_channel(int(hp_log_ch_id)) if log_ch: - color = 0x8b0000 if action_taken == "ban" else 0xff9500 + color = 0x8B0000 if action_taken == "ban" else 0xFF9500 action_label = "🔨 Banned" if action_taken == "ban" else "🔇 Muted (1 year)" + embed = discord.Embed( title="🍯 Honeypot triggered", description=f"{member.mention} wrote in the honeypot channel.", @@ -2988,25 +2963,31 @@ async def on_message(message): embed.add_field(name="Action", value=action_label, inline=True) embed.add_field(name="User", value=f"{member} (`{member.id}`)", inline=True) embed.add_field(name="Channel", value=f"<#{message.channel.id}>", inline=True) + + if action_taken == "mute": + embed.add_field(name="Mute Record ID", value=f"`{honeypot_mute_id}`", inline=True) + if preserve: embed.add_field( name="Account age on server", value=f"{days_on_server if is_old_account else 'N/A'} days (Minimum: {acc_age_min}d)", inline=False ) + if message.content: embed.add_field( name="Message content", value=message.content[:500], inline=False ) + embed.set_thumbnail(url=member.display_avatar.url) await log_ch.send(embed=embed) except Exception as e: logger.error(f"Honeypot: error sending log: {e}") - return # Never process further for honeypot channel messages - # ── End honeypot check ────────────────────────────────────────────────────── + return + # ── End honeypot check ───────────────────────────────────────────────────────── cooldown_key = (user_id, guild_id) current_time = time.time() @@ -6490,132 +6471,105 @@ async def restorewarn(ctx, warning_id: int): 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, silent: bool = False): - """Mutes a user for a specified duration (Requires Permission Level 5 or higher) - - Usage: - /mute @user 10m "Inappropriate behavior" - /mute @user 1h "Bad language" 1407754702564884622 - /mute @user 1h "Bad language" 1407754702564884622 15 - /mute @user 30m "Spamming" silent:True (silent mode - no public announcement) - - Parameters: - - user: The user to mute - - duration: Duration (10m, 1h, 2d) - - reason: Reason for the mute (can include message ID and context range) - - message_id: Optional message ID to reference (required for context_range) - - context_range: Number of context messages to archive (only works with message_id) - - silent: If True, only send ephemeral response to mod (no public message) - - Duration examples: - - 10m = 10 minutes - - 1h = 1 hour - - 2d = 2 days - - You can also specify message ID and context range in the reason: - "Bad language 1407754702564884622 15" (15 messages before/after) - - Note: context_range parameter only works when message_id is also provided! - """ - # Check if it's a slash command and defer if needed - is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction - - # For slash commands, always defer to ensure we have a response method +async def mute( + ctx, + user: discord.User, + duration: str, + reason: str = "No reason provided", + message_id: str = None, + context_range: int = 3, + silent: bool = False +): + """Mute a user for a specified duration.""" + + is_slash_command = hasattr(ctx, "interaction") and ctx.interaction is not None + if is_slash_command: - await ctx.defer(ephemeral=silent) # Defer as ephemeral if silent mode - - # Helper function for sending responses + await ctx.defer(ephemeral=silent) + async def send_response(content=None, embed=None, ephemeral=False, file=None): try: if is_slash_command: - if hasattr(ctx, 'followup') and ctx.followup: + if hasattr(ctx, "followup") and ctx.followup: await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file) - elif hasattr(ctx, 'response') and not ctx.response.is_done(): - await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, file=file) + elif hasattr(ctx, "interaction") and ctx.interaction: + await ctx.interaction.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file) else: await ctx.send(content=content, embed=embed, file=file) else: await ctx.send(content=content, embed=embed, file=file) except Exception as e: - logger.error(f"Error sending response in mute command: {e}") - # Final fallback - try basic send + logger.error(f"Error sending mute response: {e}") try: if embed: await ctx.send(embed=embed) elif content: - await ctx.send(content=content) + await ctx.send(content) except Exception as fallback_error: - logger.error(f"Fallback send also failed: {fallback_error}") - + logger.error(f"Fallback send failed: {fallback_error}") + try: - # Parse message ID and context range from reason if they look valid original_reason = reason message_data = None - parsed_context_range = 3 # Default context range, only used if message_id is provided - - # Check if reason contains potential message ID and context range + parsed_context_range = 3 + reason_words = reason.split() if len(reason_words) >= 2: - # Check for pattern: "reason text 1234567890123456789 15" - potential_msg_id = reason_words[-2] if len(reason_words) >= 2 else None - potential_context = reason_words[-1] if len(reason_words) >= 1 else None - - # Check if last two elements are message ID and context range - if (potential_msg_id and len(potential_msg_id) >= 17 and len(potential_msg_id) <= 20 and potential_msg_id.isdigit() and - potential_context and len(potential_context) <= 3 and potential_context.isdigit()): - parsed_context_range = int(potential_context) - message_id = potential_msg_id - reason = " ".join(reason_words[:-2]) # Remove both from reason - elif len(reason_words) >= 1: - # Check if reason ends with a potential message ID only - potential_msg_id = reason_words[-1] - if len(potential_msg_id) >= 17 and len(potential_msg_id) <= 20 and potential_msg_id.isdigit(): + potential_msg_id = reason_words[-2] + potential_context = reason_words[-1] + + if ( + potential_msg_id + and 17 <= len(potential_msg_id) <= 20 + and potential_msg_id.isdigit() + and potential_context + and len(potential_context) <= 3 + and potential_context.isdigit() + ): + parsed_context_range = int(potential_context) message_id = potential_msg_id - reason = " ".join(reason_words[:-1]) # Remove message ID from reason - - # Only use context_range parameter if message_id is also provided + reason = " ".join(reason_words[:-2]) + elif len(reason_words) >= 1: + potential_msg_id = reason_words[-1] + if 17 <= len(potential_msg_id) <= 20 and potential_msg_id.isdigit(): + message_id = potential_msg_id + reason = " ".join(reason_words[:-1]) + if message_id and context_range != 3: - # If message_id was provided and context_range was also set, use it parsed_context_range = context_range elif not message_id: - # If no message_id was found, reset context_range to default parsed_context_range = 3 - - # Validate and limit context range + if parsed_context_range < 1: parsed_context_range = 1 elif parsed_context_range > 25: parsed_context_range = 25 - - # Try to get message data if message ID was provided + if message_id: try: message_id_int = int(message_id) except ValueError: - await send_response(content=f"❌ Invalid message ID: {message_id}") + await send_response(content=f"❌ Invalid message ID: {message_id}", ephemeral=True) return - - # Try to get message data from current channel first + message_data = await get_message_data(ctx.channel, message_id_int, context_range=parsed_context_range) - - # If not found in current channel, try other channels + if message_data is None: - # Limit search to avoid spam - only check first 10 channels plus current channel channels_to_check = [ctx.channel] + [ch for ch in ctx.guild.text_channels[:10] if ch.id != ctx.channel.id] - for channel in channels_to_check[1:]: # Skip current channel, already checked + for channel in channels_to_check[1:]: try: message_data = await get_message_data(channel, message_id_int, context_range=parsed_context_range) if message_data is not None: break except discord.Forbidden: continue - - # Load moderator data + mod_data = await load_user_data(ctx.author.id, ctx.guild.id) - - # Check moderation rights + if not check_moderation_permission(mod_data["permission"]): embed = discord.Embed( title="❌ Insufficient Permissions", @@ -6625,7 +6579,6 @@ async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason await send_response(embed=embed, ephemeral=True) return - # Cannot mute yourself if user.id == ctx.author.id: embed = discord.Embed( title="❌ Invalid Action", @@ -6634,10 +6587,9 @@ async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason ) await send_response(embed=embed, ephemeral=True) return - - # Parse duration - time_units = {'m': 60, 'h': 3600, 'd': 86400} - if not duration or not duration[-1] in time_units or not duration[:-1].isdigit(): + + time_units = {"m": 60, "h": 3600, "d": 86400} + if not duration or duration[-1] not in time_units or not duration[:-1].isdigit(): embed = discord.Embed( title="❌ Invalid Duration", description="Invalid time format. Use: 10m, 1h, 2d", @@ -6645,11 +6597,9 @@ async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason ) await send_response(embed=embed, ephemeral=True) return - + duration_seconds = int(duration[:-1]) * time_units[duration[-1]] - end_time = datetime.now() + timedelta(seconds=duration_seconds) - - # Get member object + member = ctx.guild.get_member(user.id) if not member: embed = discord.Embed( @@ -6659,74 +6609,28 @@ async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason ) await send_response(embed=embed, ephemeral=True) return - - # Load guild settings - guild_settings = get_guild_settings(ctx.guild.id) - - # Save current roles - await save_user_roles(user.id, ctx.guild.id, member.roles) - - # Remove all roles except @everyone - roles_to_remove = [role for role in member.roles if not role.is_default()] - if roles_to_remove: - await member.remove_roles(*roles_to_remove, reason=f"Muted by {ctx.author}") - - # Get or create mute role - mute_role = await get_or_create_mute_role(ctx.guild, guild_settings) - if not mute_role: - embed = discord.Embed( - title="❌ Mute Role Error", - description="Could not find or create mute role. Check server settings.", - color=0xff0000 - ) - await send_response(embed=embed, ephemeral=True) - return - - # Add mute role - await member.add_roles(mute_role, reason=f"Muted by {ctx.author} for {duration}") - - # Update user data - user_data = await load_user_data(user.id, ctx.guild.id) - user_data["mutes"] += 1 - update_user_data(user.id, ctx.guild.id, "mutes", user_data["mutes"]) - - # Create active process for auto-unmute - process_data = { - "user_id": user.id, - "guild_id": ctx.guild.id, - "channel_id": ctx.channel.id, - "reason": reason, - "moderator_id": ctx.author.id, - "mute_role_id": mute_role.id - } - - process_uuid = create_active_process( - process_type="mute", - guild_id=ctx.guild.id, - channel_id=ctx.channel.id, - user_id=user.id, - target_id=user.id, - end_time=end_time, - data=process_data - ) - - # Save detailed mute record to database - mute_id = await save_mute_to_database( - user_id=user.id, - guild_id=ctx.guild.id, - moderator_id=ctx.author.id, + + result = await apply_mute_action( + guild=ctx.guild, + member=member, + moderator=ctx.author, + duration_seconds=duration_seconds, + duration_label=duration, reason=reason, - duration=duration, - start_time=datetime.now(), - end_time=end_time, - process_uuid=process_uuid, - channel_id=ctx.channel.id, - mute_role_id=mute_role.id, + source_channel=ctx.channel, message_data=message_data, - message_id=int(message_id) if message_id else None + message_id=int(message_id) if message_id else None, + remove_existing_roles=True, + save_removed_roles=True, + increment_mute_count=True ) - - # Create embed + + end_time = result["end_time"] + mute_id = result["mute_id"] + process_uuid = result["process_uuid"] + mute_role = result["mute_role"] + user_data = result["user_data"] + embed = discord.Embed( title="🔇 User Muted", description=f"{user.mention} has been muted.", @@ -6735,62 +6639,54 @@ async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason ) embed.add_field(name="⏱️ Duration", value=duration, inline=True) embed.add_field(name="⏰ Ends At", value=f"", inline=True) - embed.add_field(name="📝 Reason", value=reason, inline=False) + embed.add_field(name="📝 Reason", value=reason or "No reason provided", inline=False) embed.add_field(name="👮 Moderator", value=ctx.author.mention, inline=True) - embed.add_field(name="🔇 Mute Count", value=f"{user_data['mutes']}", inline=True) - - # Add message information if available + embed.add_field(name="🔇 Mute Count", value=str(user_data["mutes"]), inline=True) + if message_data: - # Handle new context message format if isinstance(message_data, dict) and "main_message" in message_data: main_msg = message_data.get("main_message") - context_msgs = message_data.get("context_messages", []) - if main_msg: message_info = f"**Message ID:** `{main_msg['id']}`\n" message_info += f"**Channel:** <#{main_msg['channel_id']}>\n" message_info += f"**Author:** {main_msg['author_name']}\n" - - if main_msg.get('content'): - content_preview = main_msg['content'][:200] + "..." if len(main_msg['content']) > 200 else main_msg['content'] + if main_msg.get("content"): + content_preview = main_msg["content"][:200] + "..." if len(main_msg["content"]) > 200 else main_msg["content"] message_info += f"**Content:** {content_preview}" - embed.add_field(name="📄 Referenced Message", value=message_info, inline=False) - - # Process attachments for archival if any - if main_msg.get('attachments'): + + if main_msg.get("attachments"): try: - attachments_data = json.loads(main_msg['attachments']) + attachments_data = json.loads(main_msg["attachments"]) if attachments_data: attachment_info = "" - for i, att in enumerate(attachments_data[:3]): # Show first 3 attachments + for i, att in enumerate(attachments_data[:3]): attachment_info += f"• {att.get('filename', 'Unknown file')}\n" if len(attachments_data) > 3: attachment_info += f"• +{len(attachments_data) - 3} more attachments" embed.add_field(name="📎 Archived Attachments", value=attachment_info, inline=False) - except: + except Exception: pass else: - # Handle old format for backward compatibility message_info = f"**Message ID:** `{message_data.get('id', 'Unknown')}`\n" message_info += f"**Channel:** <#{message_data.get('channel_id', 'Unknown')}>\n" message_info += f"**Author:** <@{message_data.get('author_id', 'Unknown')}>\n" - if message_data.get('content'): - content_preview = message_data['content'][:200] + "..." if len(message_data['content']) > 200 else message_data['content'] + if message_data.get("content"): + content_preview = message_data["content"][:200] + "..." if len(message_data["content"]) > 200 else message_data["content"] message_info += f"**Content:** {content_preview}" embed.add_field(name="📄 Referenced Message", value=message_info, inline=False) elif message_id: - embed.add_field(name="📄 Referenced Message", value=f"Message ID: `{message_id}` (Message not found or inaccessible)", inline=False) - - # Add Mute Record ID field + embed.add_field( + name="📄 Referenced Message", + value=f"Message ID: `{message_id}` (Message not found or inaccessible)", + inline=False + ) + embed.add_field(name="🆔 Mute Record ID", value=f"`{mute_id}`", inline=True) - embed.set_footer(text=f"User ID: {user.id} | Process ID: {str(process_uuid)[:8]} | Use /viewmute {mute_id} for details") embed.set_thumbnail(url=user.display_avatar.url) - - # Send response based on silent mode + if silent: - # Silent mode: ephemeral response to moderator only silent_embed = discord.Embed( title="🔇 Silent Mute Applied", description=f"{user.mention} has been muted silently.", @@ -6799,82 +6695,46 @@ async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason ) silent_embed.add_field(name="⏱️ Duration", value=duration, inline=True) silent_embed.add_field(name="⏰ Ends At", value=f"", inline=True) - silent_embed.add_field(name="📝 Reason", value=reason, inline=False) - silent_embed.add_field(name="🔇 Mute Count", value=f"{user_data['mutes']}", inline=True) + silent_embed.add_field(name="📝 Reason", value=reason or "No reason provided", inline=False) + silent_embed.add_field(name="🔇 Mute Count", value=str(user_data["mutes"]), inline=True) silent_embed.add_field(name="🆔 Mute Record ID", value=f"`{mute_id}`", inline=True) - - silent_embed.add_field(name="🔔 Actions Taken", - value="• User muted with timeout role\n• User received DM notification\n• Mod log entry created\n• No public announcement", - inline=False) + silent_embed.add_field( + name="🔔 Actions Taken", + value="• User muted\n• User received DM notification if possible\n• Mod log entry created\n• No public announcement", + inline=False + ) silent_embed.set_footer(text=f"Silent Mode • User ID: {user.id} | Use /viewmute {mute_id} for details") silent_embed.set_thumbnail(url=user.display_avatar.url) - - # Send ephemeral response - use followup since we deferred + try: if is_slash_command: - # Since we deferred with ephemeral=silent, use followup - # Make sure followup is available and properly initialized - if hasattr(ctx, 'followup') and ctx.followup is not None: + if hasattr(ctx, "followup") and ctx.followup is not None: await ctx.followup.send(embed=silent_embed, ephemeral=True) - logger.info(f"Silent mute sent via ctx.followup.send (ephemeral)") - elif hasattr(ctx, 'interaction') and ctx.interaction: - # Direct interaction followup as fallback + elif hasattr(ctx, "interaction") and ctx.interaction: await ctx.interaction.followup.send(embed=silent_embed, ephemeral=True) - logger.info(f"Silent mute sent via ctx.interaction.followup.send (ephemeral)") else: - logger.error(f"Silent mute failed: No followup available - ctx.followup: {getattr(ctx, 'followup', None)}") - raise Exception("No followup available after defer") + raise RuntimeError("No followup available after defer") else: - # For prefix commands, we can't do true ephemeral, so log error instead - logger.error(f"Silent mute attempted with prefix command - not supported") - raise Exception("Silent mode only works with slash commands") + await ctx.send(embed=silent_embed) except Exception as e: logger.error(f"Error sending silent mute response: {e}") - # Send error to mod log instead of fallback message - try: - await log_moderation_action( - guild=ctx.guild, - action_type="mute_error", - moderator=ctx.author, - target_user=user, - reason=f"Silent mute issued but ephemeral response failed: {str(e)}", - duration=duration, - additional_info={ - "Original Reason": reason, - "Mute ID": str(mute_id) if mute_id else "N/A", - "Duration": duration, - "Error Details": str(e), - "Command Type": "Slash" if is_slash_command else "Prefix", - "Fallback": "User received DM notification normally" - } - ) - except Exception as log_error: - logger.error(f"Failed to log silent mute error: {log_error}") - - # Silent mode is complete - exit here to prevent normal logging/responses - return - else: - # Normal mode: public response await send_response(embed=embed) - # Log the mute action log_additional_info = { - "Mute Count": str(user_data['mutes']), + "Mute Count": str(user_data["mutes"]), "Process ID": str(process_uuid)[:8], "Mute Record ID": str(mute_id) } - + if message_data: - # Handle new context message format if isinstance(message_data, dict) and "main_message" in message_data: main_msg = message_data.get("main_message") if main_msg: log_additional_info["Referenced Message"] = f"ID: {main_msg['id']} in <#{main_msg['channel_id']}>" else: - # Handle old format log_additional_info["Referenced Message"] = f"ID: {message_data['id']} in <#{message_data['channel_id']}>" - + await log_moderation_action( guild=ctx.guild, action_type="mute", @@ -6885,7 +6745,6 @@ async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason additional_info=log_additional_info ) - # Try to DM the user try: dm_embed = discord.Embed( title="🔇 You have been muted", @@ -6895,21 +6754,29 @@ async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason ) dm_embed.add_field(name="⏱️ Duration", value=duration, inline=True) dm_embed.add_field(name="⏰ Ends At", value=f"", inline=True) - dm_embed.add_field(name="📝 Reason", value=reason, inline=False) + dm_embed.add_field(name="📝 Reason", value=reason or "No reason provided", inline=False) dm_embed.add_field(name="👮 Moderator", value=ctx.author.display_name, inline=True) - - if message_data and message_data['content']: - content_preview = message_data['content'][:200] + "..." if len(message_data['content']) > 200 else message_data['content'] - dm_embed.add_field(name="📄 Referenced Message", value=f"```{content_preview}```", inline=False) - + + if message_data: + preview_content = None + if isinstance(message_data, dict) and "main_message" in message_data: + main_msg = message_data.get("main_message") + if main_msg: + preview_content = main_msg.get("content") + else: + preview_content = message_data.get("content") + + if preview_content: + content_preview = preview_content[:200] + "..." if len(preview_content) > 200 else preview_content + dm_embed.add_field(name="📄 Referenced Message", value=f"```{content_preview}```", inline=False) + dm_embed.set_footer(text=f"Server: {ctx.guild.name}") await user.send(embed=dm_embed) except discord.Forbidden: - pass # User has DMs disabled - - # Log the action + pass + logger.info(f"User {user.id} muted by {ctx.author.id} in guild {ctx.guild.id} for {duration}. Reason: {reason}") - + except Exception as e: logger.error(f"Error in mute command: {e}") embed = discord.Embed( @@ -6917,7 +6784,88 @@ async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason description="An error occurred while processing the mute. Please try again.", color=0xff0000 ) - await send_response(embed=embed) + await send_response(embed=embed, ephemeral=True) + + +async def apply_mute_action( + *, + guild: discord.Guild, + member: discord.Member, + moderator, + duration_seconds: int, + duration_label: str, + reason: str, + source_channel=None, + message_data: dict | None = None, + message_id: int | None = None, + remove_existing_roles: bool = True, + save_removed_roles: bool = True, + increment_mute_count: bool = True +): + guild_settings = get_guild_settings(guild.id) + + if save_removed_roles: + await save_user_roles(member.id, guild.id, member.roles) + + if remove_existing_roles: + roles_to_remove = [role for role in member.roles if not role.is_default()] + if roles_to_remove: + await member.remove_roles(*roles_to_remove, reason=f"Muted by {moderator}") + + mute_role = await get_or_create_mute_role(guild, guild_settings) + if not mute_role: + raise RuntimeError("Could not find or create mute role") + + await member.add_roles(mute_role, reason=f"Muted by {moderator} for {duration_label}") + + user_data = await load_user_data(member.id, guild.id) + if increment_mute_count: + user_data["mutes"] += 1 + update_user_data(member.id, guild.id, "mutes", user_data["mutes"]) + + end_time = datetime.now() + timedelta(seconds=duration_seconds) + + process_data = { + "user_id": member.id, + "guild_id": guild.id, + "channel_id": source_channel.id if source_channel else None, + "reason": reason, + "moderator_id": moderator.id, + "mute_role_id": mute_role.id + } + + process_uuid = create_active_process( + process_type="mute", + guild_id=guild.id, + channel_id=source_channel.id if source_channel else 0, + user_id=member.id, + target_id=member.id, + end_time=end_time, + data=process_data + ) + + mute_id = await save_mute_to_database( + user_id=member.id, + guild_id=guild.id, + moderator_id=moderator.id, + reason=reason, + duration=duration_label, + start_time=datetime.now(), + end_time=end_time, + process_uuid=process_uuid, + channel_id=source_channel.id if source_channel else None, + mute_role_id=mute_role.id, + message_data=message_data, + message_id=message_id + ) + + return { + "mute_role": mute_role, + "user_data": user_data, + "end_time": end_time, + "process_uuid": process_uuid, + "mute_id": mute_id + } @client.hybrid_command() async def unmute(ctx, user: discord.User):