diff --git a/bot.py b/bot.py index cc82b7c..7eb971e 100644 --- a/bot.py +++ b/bot.py @@ -2894,12 +2894,33 @@ async def on_message(message): member_role_ids = {role.id for role in member.roles} if not (ignore_role_ids & member_role_ids): + archived_message_data = { + "id": message.id, + "channel_id": message.channel.id, + "author_id": member.id, + "author_name": str(member), + "content": message.content, + "attachments": json.dumps( + [ + { + "filename": a.filename, + "url": a.url, + "content_type": a.content_type, + "size": a.size + } + for a in message.attachments + ] + ) if message.attachments else "[]" + } + try: await message.delete() except Exception: pass action_taken = None + honeypot_mute_id = None + acc_age_min = int(guild_settings.get("honeypot_acc_age_min") or 30) preserve = guild_settings.get("honeypot_preserve_old_accounts", False) @@ -2913,7 +2934,7 @@ async def on_message(message): if is_old_account: try: - result = await apply_mute_action( + result = await apply_full_mute( guild=message.guild, member=member, moderator=client.user, @@ -2921,14 +2942,16 @@ async def on_message(message): duration_label="365d", reason="Honeypot: wrote in honeypot channel (protected old member)", source_channel=message.channel, - message_data=None, + message_data=archived_message_data, message_id=message.id, - remove_existing_roles=False, - save_removed_roles=False, + send_dm=True, + log_action=True, + remove_existing_roles=True, + save_current_roles=True, increment_mute_count=True ) - action_taken = "mute" honeypot_mute_id = result["mute_id"] + action_taken = "mute" except discord.Forbidden: logger.warning(f"Honeypot: no permission to mute {member.id} in guild {guild_id}") except Exception as e: @@ -2964,7 +2987,7 @@ async def on_message(message): 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": + if honeypot_mute_id: embed.add_field(name="Mute Record ID", value=f"`{honeypot_mute_id}`", inline=True) if preserve: @@ -2987,7 +3010,7 @@ async def on_message(message): logger.error(f"Honeypot: error sending log: {e}") return - # ── End honeypot check ───────────────────────────────────────────────────────── + # ── End honeypot check ───────────────────────────────────────────────────────── cooldown_key = (user_id, guild_id) current_time = time.time() @@ -6484,7 +6507,7 @@ async def mute( context_range: int = 3, silent: bool = False ): - """Mute a user for a specified duration.""" + """Mutes a user for a specified duration (Requires Permission Level 5 or higher).""" is_slash_command = hasattr(ctx, "interaction") and ctx.interaction is not None @@ -6503,17 +6526,16 @@ async def mute( else: await ctx.send(content=content, embed=embed, file=file) except Exception as e: - logger.error(f"Error sending mute response: {e}") + logger.error(f"Error sending response in mute command: {e}") try: if embed: await ctx.send(embed=embed) elif content: - await ctx.send(content) + await ctx.send(content=content) except Exception as fallback_error: - logger.error(f"Fallback send failed: {fallback_error}") + logger.error(f"Fallback send also failed: {fallback_error}") try: - original_reason = reason message_data = None parsed_context_range = 3 @@ -6523,16 +6545,13 @@ async def mute( 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() + 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[:-2]) + elif len(reason_words) >= 1: potential_msg_id = reason_words[-1] if 17 <= len(potential_msg_id) <= 20 and potential_msg_id.isdigit(): @@ -6610,7 +6629,7 @@ async def mute( await send_response(embed=embed, ephemeral=True) return - result = await apply_mute_action( + result = await apply_full_mute( guild=ctx.guild, member=member, moderator=ctx.author, @@ -6620,15 +6639,16 @@ async def mute( source_channel=ctx.channel, message_data=message_data, message_id=int(message_id) if message_id else None, + send_dm=True, + log_action=True, remove_existing_roles=True, - save_removed_roles=True, + save_current_roles=True, increment_mute_count=True ) 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( @@ -6641,18 +6661,22 @@ async def mute( embed.add_field(name="⏰ Ends At", value=f"", inline=True) 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=str(user_data["mutes"]), inline=True) + embed.add_field(name="🔇 Mute Count", value=f"{user_data['mutes']}", inline=True) if message_data: 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"] message_info += f"**Content:** {content_preview}" + embed.add_field(name="📄 Referenced Message", value=message_info, inline=False) if main_msg.get("attachments"): @@ -6660,7 +6684,7 @@ async def mute( attachments_data = json.loads(main_msg["attachments"]) if attachments_data: attachment_info = "" - for i, att in enumerate(attachments_data[:3]): + for att in 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" @@ -6696,84 +6720,20 @@ async def mute( 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 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 Count", value=f"{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\n• User received DM notification if possible\n• Mod log entry created\n• No public announcement", + value="• User muted\n• Roles saved\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) - try: - if is_slash_command: - if hasattr(ctx, "followup") and ctx.followup is not None: - await ctx.followup.send(embed=silent_embed, ephemeral=True) - elif hasattr(ctx, "interaction") and ctx.interaction: - await ctx.interaction.followup.send(embed=silent_embed, ephemeral=True) - else: - raise RuntimeError("No followup available after defer") - else: - await ctx.send(embed=silent_embed) - except Exception as e: - logger.error(f"Error sending silent mute response: {e}") - else: - await send_response(embed=embed) + await send_response(embed=silent_embed, ephemeral=True) + return - log_additional_info = { - "Mute Count": str(user_data["mutes"]), - "Process ID": str(process_uuid)[:8], - "Mute Record ID": str(mute_id) - } - - if message_data: - 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: - 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", - moderator=ctx.author, - target_user=user, - reason=reason, - duration=duration, - additional_info=log_additional_info - ) - - try: - dm_embed = discord.Embed( - title="🔇 You have been muted", - description=f"You have been muted in **{ctx.guild.name}**", - color=0xff0000, - timestamp=datetime.now() - ) - 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 or "No reason provided", inline=False) - dm_embed.add_field(name="👮 Moderator", value=ctx.author.display_name, inline=True) - - 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 + await send_response(embed=embed) logger.info(f"User {user.id} muted by {ctx.author.id} in guild {ctx.guild.id} for {duration}. Reason: {reason}") @@ -6786,8 +6746,7 @@ async def mute( ) await send_response(embed=embed, ephemeral=True) - -async def apply_mute_action( +async def apply_full_mute( *, guild: discord.Guild, member: discord.Member, @@ -6795,16 +6754,18 @@ async def apply_mute_action( duration_seconds: int, duration_label: str, reason: str, - source_channel=None, + source_channel: discord.TextChannel | None = None, message_data: dict | None = None, message_id: int | None = None, + send_dm: bool = True, + log_action: bool = True, remove_existing_roles: bool = True, - save_removed_roles: bool = True, + save_current_roles: bool = True, increment_mute_count: bool = True ): guild_settings = get_guild_settings(guild.id) - if save_removed_roles: + if save_current_roles: await save_user_roles(member.id, guild.id, member.roles) if remove_existing_roles: @@ -6823,7 +6784,8 @@ async def apply_mute_action( user_data["mutes"] += 1 update_user_data(member.id, guild.id, "mutes", user_data["mutes"]) - end_time = datetime.now() + timedelta(seconds=duration_seconds) + start_time = datetime.now() + end_time = start_time + timedelta(seconds=duration_seconds) process_data = { "user_id": member.id, @@ -6850,7 +6812,7 @@ async def apply_mute_action( moderator_id=moderator.id, reason=reason, duration=duration_label, - start_time=datetime.now(), + start_time=start_time, end_time=end_time, process_uuid=process_uuid, channel_id=source_channel.id if source_channel else None, @@ -6859,9 +6821,69 @@ async def apply_mute_action( message_id=message_id ) + if log_action: + additional_info = { + "Mute Count": str(user_data["mutes"]), + "Process ID": str(process_uuid)[:8], + "Mute Record ID": str(mute_id) + } + + if message_data: + if isinstance(message_data, dict) and "main_message" in message_data: + main_msg = message_data.get("main_message") + if main_msg: + additional_info["Referenced Message"] = f"ID: {main_msg['id']} in <#{main_msg['channel_id']}>" + else: + if message_data.get("id") and message_data.get("channel_id"): + additional_info["Referenced Message"] = f"ID: {message_data['id']} in <#{message_data['channel_id']}>" + + await log_moderation_action( + guild=guild, + action_type="mute", + moderator=moderator, + target_user=member, + reason=reason, + duration=duration_label, + additional_info=additional_info + ) + + if send_dm: + try: + dm_embed = discord.Embed( + title="🔇 You have been muted", + description=f"You have been muted in **{guild.name}**", + color=0xff0000, + timestamp=datetime.now() + ) + dm_embed.add_field(name="⏱️ Duration", value=duration_label, inline=True) + dm_embed.add_field(name="⏰ Ends At", value=f"", inline=True) + dm_embed.add_field(name="📝 Reason", value=reason or "No reason provided", inline=False) + dm_embed.add_field(name="👮 Moderator", value=getattr(moderator, "display_name", str(moderator)), inline=True) + + preview_content = None + if message_data: + 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: {guild.name}") + await member.send(embed=dm_embed) + except discord.Forbidden: + pass + except Exception as e: + logger.error(f"Failed to DM muted user {member.id}: {e}") + return { "mute_role": mute_role, "user_data": user_data, + "start_time": start_time, "end_time": end_time, "process_uuid": process_uuid, "mute_id": mute_id