modified: bot.py
This commit is contained in:
556
bot.py
556
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"<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="🔇 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"<t:{int(end_time.timestamp())}: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"<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)
|
||||
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user