modified: bot.py

This commit is contained in:
SimolZimol
2026-06-15 22:30:27 +02:00
parent ba9e40d105
commit 7edf0e34a6

498
bot.py
View File

@@ -2874,11 +2874,12 @@ async def on_message(message):
guild_id = message.guild.id guild_id = message.guild.id
member = message.author # Das Member-Objekt für Datenaktualisierung 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) guild_settings = get_guild_settings(guild_id)
if guild_settings.get("honeypot_enabled") and guild_settings.get("honeypot_channel_id"): if guild_settings.get("honeypot_enabled") and guild_settings.get("honeypot_channel_id"):
if message.channel.id == int(guild_settings["honeypot_channel_id"]): if message.channel.id == int(guild_settings["honeypot_channel_id"]):
# Build ignore-role set
ignore_role_ids = set() ignore_role_ids = set()
raw_ignore = guild_settings.get("honeypot_ignore_roles") raw_ignore = guild_settings.get("honeypot_ignore_roles")
if raw_ignore: if raw_ignore:
@@ -2887,9 +2888,12 @@ async def on_message(message):
except Exception: except Exception:
pass 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} member_role_ids = {role.id for role in member.roles}
if not (ignore_role_ids & member_role_ids): if not (ignore_role_ids & member_role_ids):
# Delete the honeypot message
try: try:
await message.delete() await message.delete()
except Exception: except Exception:
@@ -2899,67 +2903,36 @@ async def on_message(message):
acc_age_min = int(guild_settings.get("honeypot_acc_age_min") or 30) acc_age_min = int(guild_settings.get("honeypot_acc_age_min") or 30)
preserve = guild_settings.get("honeypot_preserve_old_accounts", False) preserve = guild_settings.get("honeypot_preserve_old_accounts", False)
# Determine: old-account mute vs ban
if preserve and member.joined_at: if preserve and member.joined_at:
now_aware = datetime.now(member.joined_at.tzinfo) now_aware = datetime.now(member.joined_at.tzinfo)
days_on_server = (now_aware - member.joined_at).days days_on_server = (now_aware - member.joined_at).days
is_old_account = days_on_server >= acc_age_min is_old_account = days_on_server >= acc_age_min
else: else:
days_on_server = None
is_old_account = False is_old_account = False
if is_old_account: if is_old_account:
# Apply 1-year (365 days) mute via the mute system try:
honeypot_role = None result = await apply_mute_action(
hp_role_id = guild_settings.get("honeypot_get_role") guild=message.guild,
if hp_role_id: member=member,
try: moderator=client.user,
honeypot_role = message.guild.get_role(int(hp_role_id)) duration_seconds=365 * 86400,
except Exception: duration_label="365d",
pass reason="Honeypot: wrote in honeypot channel (protected old member)",
if honeypot_role is None: source_channel=message.channel,
honeypot_role = await get_or_create_mute_role(message.guild, guild_settings) message_data=None,
message_id=message.id,
if honeypot_role: remove_existing_roles=False,
try: save_removed_roles=False,
await member.add_roles( increment_mute_count=True
honeypot_role, )
reason="Honeypot: wrote in honeypot channel (account protected as old member)" action_taken = "mute"
) honeypot_mute_id = result["mute_id"]
# Persist mute record for 365 days except discord.Forbidden:
start_time = datetime.now() logger.warning(f"Honeypot: no permission to mute {member.id} in guild {guild_id}")
end_time = start_time + timedelta(days=365) except Exception as e:
process_data = { logger.error(f"Honeypot mute failed for {member.id} in guild {guild_id}: {e}")
"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}")
else: else:
try: try:
await message.guild.ban( await message.guild.ban(
@@ -2970,15 +2943,17 @@ async def on_message(message):
action_taken = "ban" action_taken = "ban"
except discord.Forbidden: except discord.Forbidden:
logger.warning(f"Honeypot: no permission to ban {member.id} in guild {guild_id}") 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") hp_log_ch_id = guild_settings.get("honeypot_log_channel_id")
if action_taken and hp_log_ch_id: if action_taken and hp_log_ch_id:
try: try:
log_ch = message.guild.get_channel(int(hp_log_ch_id)) log_ch = message.guild.get_channel(int(hp_log_ch_id))
if log_ch: 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)" action_label = "🔨 Banned" if action_taken == "ban" else "🔇 Muted (1 year)"
embed = discord.Embed( embed = discord.Embed(
title="🍯 Honeypot triggered", title="🍯 Honeypot triggered",
description=f"{member.mention} wrote in the honeypot channel.", 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="Action", value=action_label, inline=True)
embed.add_field(name="User", value=f"{member} (`{member.id}`)", 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) 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: if preserve:
embed.add_field( embed.add_field(
name="Account age on server", name="Account age on server",
value=f"{days_on_server if is_old_account else 'N/A'} days (Minimum: {acc_age_min}d)", value=f"{days_on_server if is_old_account else 'N/A'} days (Minimum: {acc_age_min}d)",
inline=False inline=False
) )
if message.content: if message.content:
embed.add_field( embed.add_field(
name="Message content", name="Message content",
value=message.content[:500], value=message.content[:500],
inline=False inline=False
) )
embed.set_thumbnail(url=member.display_avatar.url) embed.set_thumbnail(url=member.display_avatar.url)
await log_ch.send(embed=embed) await log_ch.send(embed=embed)
except Exception as e: except Exception as e:
logger.error(f"Honeypot: error sending log: {e}") logger.error(f"Honeypot: error sending log: {e}")
return # Never process further for honeypot channel messages return
# ── End honeypot check ────────────────────────────────────────────────────── # ── End honeypot check ─────────────────────────────────────────────────────────
cooldown_key = (user_id, guild_id) cooldown_key = (user_id, guild_id)
current_time = time.time() current_time = time.time()
@@ -6491,120 +6472,95 @@ async def restorewarn(ctx, warning_id: int):
if connection: if connection:
close_database_connection(connection) close_database_connection(connection)
@client.hybrid_command() @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): async def mute(
"""Mutes a user for a specified duration (Requires Permission Level 5 or higher) 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."""
Usage: is_slash_command = hasattr(ctx, "interaction") and ctx.interaction is not None
/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
if is_slash_command: if is_slash_command:
await ctx.defer(ephemeral=silent) # Defer as ephemeral if silent mode await ctx.defer(ephemeral=silent)
# Helper function for sending responses
async def send_response(content=None, embed=None, ephemeral=False, file=None): async def send_response(content=None, embed=None, ephemeral=False, file=None):
try: try:
if is_slash_command: 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) await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file)
elif hasattr(ctx, 'response') and not ctx.response.is_done(): elif hasattr(ctx, "interaction") and ctx.interaction:
await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, file=file) await ctx.interaction.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file)
else: else:
await ctx.send(content=content, embed=embed, file=file) await ctx.send(content=content, embed=embed, file=file)
else: else:
await ctx.send(content=content, embed=embed, file=file) await ctx.send(content=content, embed=embed, file=file)
except Exception as e: except Exception as e:
logger.error(f"Error sending response in mute command: {e}") logger.error(f"Error sending mute response: {e}")
# Final fallback - try basic send
try: try:
if embed: if embed:
await ctx.send(embed=embed) await ctx.send(embed=embed)
elif content: elif content:
await ctx.send(content=content) await ctx.send(content)
except Exception as fallback_error: except Exception as fallback_error:
logger.error(f"Fallback send also failed: {fallback_error}") logger.error(f"Fallback send failed: {fallback_error}")
try: try:
# Parse message ID and context range from reason if they look valid
original_reason = reason original_reason = reason
message_data = None message_data = None
parsed_context_range = 3 # Default context range, only used if message_id is provided parsed_context_range = 3
# Check if reason contains potential message ID and context range
reason_words = reason.split() reason_words = reason.split()
if len(reason_words) >= 2: if len(reason_words) >= 2:
# Check for pattern: "reason text 1234567890123456789 15" potential_msg_id = reason_words[-2]
potential_msg_id = reason_words[-2] if len(reason_words) >= 2 else None potential_context = reason_words[-1]
potential_context = reason_words[-1] if len(reason_words) >= 1 else None
# Check if last two elements are message ID and context range if (
if (potential_msg_id and len(potential_msg_id) >= 17 and len(potential_msg_id) <= 20 and potential_msg_id.isdigit() and potential_msg_id
potential_context and len(potential_context) <= 3 and potential_context.isdigit()): and 17 <= len(potential_msg_id) <= 20
parsed_context_range = int(potential_context) and potential_msg_id.isdigit()
message_id = potential_msg_id and potential_context
reason = " ".join(reason_words[:-2]) # Remove both from reason and len(potential_context) <= 3
elif len(reason_words) >= 1: and potential_context.isdigit()
# Check if reason ends with a potential message ID only ):
potential_msg_id = reason_words[-1] parsed_context_range = int(potential_context)
if len(potential_msg_id) >= 17 and len(potential_msg_id) <= 20 and potential_msg_id.isdigit():
message_id = potential_msg_id message_id = potential_msg_id
reason = " ".join(reason_words[:-1]) # Remove message ID from reason 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])
# Only use context_range parameter if message_id is also provided
if message_id and context_range != 3: 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 parsed_context_range = context_range
elif not message_id: elif not message_id:
# If no message_id was found, reset context_range to default
parsed_context_range = 3 parsed_context_range = 3
# Validate and limit context range
if parsed_context_range < 1: if parsed_context_range < 1:
parsed_context_range = 1 parsed_context_range = 1
elif parsed_context_range > 25: elif parsed_context_range > 25:
parsed_context_range = 25 parsed_context_range = 25
# Try to get message data if message ID was provided
if message_id: if message_id:
try: try:
message_id_int = int(message_id) message_id_int = int(message_id)
except ValueError: 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 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) 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: 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] 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: try:
message_data = await get_message_data(channel, message_id_int, context_range=parsed_context_range) message_data = await get_message_data(channel, message_id_int, context_range=parsed_context_range)
if message_data is not None: if message_data is not None:
@@ -6612,10 +6568,8 @@ async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason
except discord.Forbidden: except discord.Forbidden:
continue continue
# Load moderator data
mod_data = await load_user_data(ctx.author.id, ctx.guild.id) mod_data = await load_user_data(ctx.author.id, ctx.guild.id)
# Check moderation rights
if not check_moderation_permission(mod_data["permission"]): if not check_moderation_permission(mod_data["permission"]):
embed = discord.Embed( embed = discord.Embed(
title="❌ Insufficient Permissions", 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) await send_response(embed=embed, ephemeral=True)
return return
# Cannot mute yourself
if user.id == ctx.author.id: if user.id == ctx.author.id:
embed = discord.Embed( embed = discord.Embed(
title="❌ Invalid Action", title="❌ Invalid Action",
@@ -6635,9 +6588,8 @@ async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason
await send_response(embed=embed, ephemeral=True) await send_response(embed=embed, ephemeral=True)
return return
# Parse duration time_units = {"m": 60, "h": 3600, "d": 86400}
time_units = {'m': 60, 'h': 3600, 'd': 86400} if not duration or duration[-1] not in time_units or not duration[:-1].isdigit():
if not duration or not duration[-1] in time_units or not duration[:-1].isdigit():
embed = discord.Embed( embed = discord.Embed(
title="❌ Invalid Duration", title="❌ Invalid Duration",
description="Invalid time format. Use: 10m, 1h, 2d", description="Invalid time format. Use: 10m, 1h, 2d",
@@ -6647,9 +6599,7 @@ async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason
return return
duration_seconds = int(duration[:-1]) * time_units[duration[-1]] 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) member = ctx.guild.get_member(user.id)
if not member: if not member:
embed = discord.Embed( embed = discord.Embed(
@@ -6660,73 +6610,27 @@ async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason
await send_response(embed=embed, ephemeral=True) await send_response(embed=embed, ephemeral=True)
return return
# Load guild settings result = await apply_mute_action(
guild_settings = get_guild_settings(ctx.guild.id) guild=ctx.guild,
member=member,
# Save current roles moderator=ctx.author,
await save_user_roles(user.id, ctx.guild.id, member.roles) duration_seconds=duration_seconds,
duration_label=duration,
# 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,
reason=reason, reason=reason,
duration=duration, source_channel=ctx.channel,
start_time=datetime.now(),
end_time=end_time,
process_uuid=process_uuid,
channel_id=ctx.channel.id,
mute_role_id=mute_role.id,
message_data=message_data, 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( embed = discord.Embed(
title="🔇 User Muted", title="🔇 User Muted",
description=f"{user.mention} has been 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="⏱️ Duration", value=duration, inline=True)
embed.add_field(name="⏰ Ends At", value=f"<t:{int(end_time.timestamp())}:F>", inline=True) embed.add_field(name="⏰ Ends At", value=f"<t:{int(end_time.timestamp())}: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="👮 Moderator", value=ctx.author.mention, inline=True)
embed.add_field(name="🔇 Mute Count", value=f"{user_data['mutes']}", inline=True) embed.add_field(name="🔇 Mute Count", value=str(user_data["mutes"]), inline=True)
# Add message information if available
if message_data: if message_data:
# Handle new context message format
if isinstance(message_data, dict) and "main_message" in message_data: if isinstance(message_data, dict) and "main_message" in message_data:
main_msg = message_data.get("main_message") main_msg = message_data.get("main_message")
context_msgs = message_data.get("context_messages", [])
if main_msg: if main_msg:
message_info = f"**Message ID:** `{main_msg['id']}`\n" message_info = f"**Message ID:** `{main_msg['id']}`\n"
message_info += f"**Channel:** <#{main_msg['channel_id']}>\n" message_info += f"**Channel:** <#{main_msg['channel_id']}>\n"
message_info += f"**Author:** {main_msg['author_name']}\n" message_info += f"**Author:** {main_msg['author_name']}\n"
if main_msg.get("content"):
if main_msg.get('content'): content_preview = main_msg["content"][:200] + "..." if len(main_msg["content"]) > 200 else main_msg["content"]
content_preview = main_msg['content'][:200] + "..." if len(main_msg['content']) > 200 else main_msg['content']
message_info += f"**Content:** {content_preview}" message_info += f"**Content:** {content_preview}"
embed.add_field(name="📄 Referenced Message", value=message_info, inline=False) 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: try:
attachments_data = json.loads(main_msg['attachments']) attachments_data = json.loads(main_msg["attachments"])
if attachments_data: if attachments_data:
attachment_info = "" 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" attachment_info += f"{att.get('filename', 'Unknown file')}\n"
if len(attachments_data) > 3: if len(attachments_data) > 3:
attachment_info += f"• +{len(attachments_data) - 3} more attachments" attachment_info += f"• +{len(attachments_data) - 3} more attachments"
embed.add_field(name="📎 Archived Attachments", value=attachment_info, inline=False) embed.add_field(name="📎 Archived Attachments", value=attachment_info, inline=False)
except: except Exception:
pass pass
else: else:
# Handle old format for backward compatibility
message_info = f"**Message ID:** `{message_data.get('id', 'Unknown')}`\n" 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"**Channel:** <#{message_data.get('channel_id', 'Unknown')}>\n"
message_info += f"**Author:** <@{message_data.get('author_id', 'Unknown')}>\n" message_info += f"**Author:** <@{message_data.get('author_id', 'Unknown')}>\n"
if message_data.get('content'): if message_data.get("content"):
content_preview = message_data['content'][:200] + "..." if len(message_data['content']) > 200 else message_data['content'] content_preview = message_data["content"][:200] + "..." if len(message_data["content"]) > 200 else message_data["content"]
message_info += f"**Content:** {content_preview}" message_info += f"**Content:** {content_preview}"
embed.add_field(name="📄 Referenced Message", value=message_info, inline=False) embed.add_field(name="📄 Referenced Message", value=message_info, inline=False)
elif message_id: elif message_id:
embed.add_field(name="📄 Referenced Message", value=f"Message ID: `{message_id}` (Message not found or inaccessible)", inline=False) 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="🆔 Mute Record ID", value=f"`{mute_id}`", inline=True) 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_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) embed.set_thumbnail(url=user.display_avatar.url)
# Send response based on silent mode
if silent: if silent:
# Silent mode: ephemeral response to moderator only
silent_embed = discord.Embed( silent_embed = discord.Embed(
title="🔇 Silent Mute Applied", title="🔇 Silent Mute Applied",
description=f"{user.mention} has been muted silently.", description=f"{user.mention} has been muted silently.",
@@ -6799,80 +6695,44 @@ 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="⏱️ Duration", value=duration, inline=True)
silent_embed.add_field(name="⏰ Ends At", value=f"<t:{int(end_time.timestamp())}:F>", inline=True) silent_embed.add_field(name="⏰ Ends At", value=f"<t:{int(end_time.timestamp())}:F>", inline=True)
silent_embed.add_field(name="📝 Reason", value=reason, inline=False) silent_embed.add_field(name="📝 Reason", value=reason or "No reason provided", inline=False)
silent_embed.add_field(name="🔇 Mute Count", value=f"{user_data['mutes']}", inline=True) 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="🆔 Mute Record ID", value=f"`{mute_id}`", inline=True)
silent_embed.add_field(
silent_embed.add_field(name="🔔 Actions Taken", name="🔔 Actions Taken",
value="• User muted with timeout role\n• User received DM notification\n• Mod log entry created\n• No public announcement", value="• User muted\n• User received DM notification if possible\n• Mod log entry created\n• No public announcement",
inline=False) inline=False
)
silent_embed.set_footer(text=f"Silent Mode • User ID: {user.id} | Use /viewmute {mute_id} for details") 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) silent_embed.set_thumbnail(url=user.display_avatar.url)
# Send ephemeral response - use followup since we deferred
try: try:
if is_slash_command: if is_slash_command:
# Since we deferred with ephemeral=silent, use followup if hasattr(ctx, "followup") and ctx.followup is not None:
# Make sure followup is available and properly initialized
if hasattr(ctx, 'followup') and ctx.followup is not None:
await ctx.followup.send(embed=silent_embed, ephemeral=True) 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:
elif hasattr(ctx, 'interaction') and ctx.interaction:
# Direct interaction followup as fallback
await ctx.interaction.followup.send(embed=silent_embed, ephemeral=True) await ctx.interaction.followup.send(embed=silent_embed, ephemeral=True)
logger.info(f"Silent mute sent via ctx.interaction.followup.send (ephemeral)")
else: else:
logger.error(f"Silent mute failed: No followup available - ctx.followup: {getattr(ctx, 'followup', None)}") raise RuntimeError("No followup available after defer")
raise Exception("No followup available after defer")
else: else:
# For prefix commands, we can't do true ephemeral, so log error instead await ctx.send(embed=silent_embed)
logger.error(f"Silent mute attempted with prefix command - not supported")
raise Exception("Silent mode only works with slash commands")
except Exception as e: except Exception as e:
logger.error(f"Error sending silent mute response: {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: else:
# Normal mode: public response
await send_response(embed=embed) await send_response(embed=embed)
# Log the mute action
log_additional_info = { log_additional_info = {
"Mute Count": str(user_data['mutes']), "Mute Count": str(user_data["mutes"]),
"Process ID": str(process_uuid)[:8], "Process ID": str(process_uuid)[:8],
"Mute Record ID": str(mute_id) "Mute Record ID": str(mute_id)
} }
if message_data: if message_data:
# Handle new context message format
if isinstance(message_data, dict) and "main_message" in message_data: if isinstance(message_data, dict) and "main_message" in message_data:
main_msg = message_data.get("main_message") main_msg = message_data.get("main_message")
if main_msg: if main_msg:
log_additional_info["Referenced Message"] = f"ID: {main_msg['id']} in <#{main_msg['channel_id']}>" log_additional_info["Referenced Message"] = f"ID: {main_msg['id']} in <#{main_msg['channel_id']}>"
else: else:
# Handle old format
log_additional_info["Referenced Message"] = f"ID: {message_data['id']} in <#{message_data['channel_id']}>" log_additional_info["Referenced Message"] = f"ID: {message_data['id']} in <#{message_data['channel_id']}>"
await log_moderation_action( await log_moderation_action(
@@ -6885,7 +6745,6 @@ async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason
additional_info=log_additional_info additional_info=log_additional_info
) )
# Try to DM the user
try: try:
dm_embed = discord.Embed( dm_embed = discord.Embed(
title="🔇 You have been muted", title="🔇 You have been muted",
@@ -6895,19 +6754,27 @@ 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="⏱️ Duration", value=duration, inline=True)
dm_embed.add_field(name="⏰ Ends At", value=f"<t:{int(end_time.timestamp())}:F>", inline=True) dm_embed.add_field(name="⏰ Ends At", value=f"<t:{int(end_time.timestamp())}: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) dm_embed.add_field(name="👮 Moderator", value=ctx.author.display_name, inline=True)
if message_data and message_data['content']: if message_data:
content_preview = message_data['content'][:200] + "..." if len(message_data['content']) > 200 else message_data['content'] preview_content = None
dm_embed.add_field(name="📄 Referenced Message", value=f"```{content_preview}```", inline=False) 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}") dm_embed.set_footer(text=f"Server: {ctx.guild.name}")
await user.send(embed=dm_embed) await user.send(embed=dm_embed)
except discord.Forbidden: except discord.Forbidden:
pass # User has DMs disabled pass
# Log the action
logger.info(f"User {user.id} muted by {ctx.author.id} in guild {ctx.guild.id} for {duration}. Reason: {reason}") logger.info(f"User {user.id} muted by {ctx.author.id} in guild {ctx.guild.id} for {duration}. Reason: {reason}")
except Exception as e: except Exception as e:
@@ -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.", description="An error occurred while processing the mute. Please try again.",
color=0xff0000 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() @client.hybrid_command()
async def unmute(ctx, user: discord.User): async def unmute(ctx, user: discord.User):