modified: bot.py

This commit is contained in:
SimolZimol
2026-06-22 17:45:59 +02:00
parent 7b8c82869c
commit 96f68c08ce

488
bot.py
View File

@@ -33,6 +33,8 @@ from urllib.parse import urlparse
load_dotenv() load_dotenv()
ATTACHMENT_BASE_PATH = "attachments"
DB_HOST = os.getenv("DB_HOST") DB_HOST = os.getenv("DB_HOST")
DB_PORT = os.getenv("DB_PORT") DB_PORT = os.getenv("DB_PORT")
DB_USER = os.getenv("DB_USER") DB_USER = os.getenv("DB_USER")
@@ -462,6 +464,49 @@ def save_global_permission(user_id, permission_level):
#----------------------------------------------------------------------------------------------------------- #-----------------------------------------------------------------------------------------------------------
async def download_message_attachments(
message: discord.Message,
guild_id: int,
subfolder: str # z.B. mute_id oder "honeypot/user_id"
) -> list[dict]:
"""
Downloads all attachments from a Discord message to local disk.
Returns a list of dicts with local path, filename, content_type and size.
"""
saved_attachments = []
if not message.attachments:
return saved_attachments
save_dir = os.path.join(ATTACHMENT_BASE_PATH, str(guild_id), subfolder)
os.makedirs(save_dir, exist_ok=True)
for attachment in message.attachments:
try:
local_filename = f"{attachment.id}_{attachment.filename}"
local_path = os.path.join(save_dir, local_filename)
# discord.py's built-in save method — downloads via aiohttp internally
await attachment.save(local_path)
saved_attachments.append({
"filename": attachment.filename,
"local_path": local_path,
"content_type": attachment.content_type or "application/octet-stream",
"size": attachment.size,
"original_url": attachment.url,
"attachment_id": str(attachment.id)
})
logger.info(f"Saved attachment {attachment.filename} to {local_path}")
except discord.HTTPException as e:
logger.error(f"HTTP error downloading attachment {attachment.filename}: {e}")
except Exception as e:
logger.error(f"Failed to save attachment {attachment.filename}: {e}")
return saved_attachments
# Active Processes System - Robust system for storing and managing active processes # Active Processes System - Robust system for storing and managing active processes
def create_active_process(process_type, guild_id, channel_id=None, user_id=None, target_id=None, def create_active_process(process_type, guild_id, channel_id=None, user_id=None, target_id=None,
start_time=None, end_time=None, status="active", data=None, metadata=None): start_time=None, end_time=None, status="active", data=None, metadata=None):
@@ -4775,54 +4820,57 @@ async def reactivate_warning(warning_id):
async def get_message_data(channel, message_id, context_range=3): async def get_message_data(channel, message_id, context_range=3):
"""Retrieves and processes message data for warning documentation with context messages""" """Retrieves and processes message data for warning documentation with context messages"""
try: try:
# Get the main message
main_message = await channel.fetch_message(message_id) main_message = await channel.fetch_message(message_id)
# Get context messages (before and after)
context_messages = [] context_messages = []
try: try:
# Get messages around the target message
async for msg in channel.history(limit=context_range * 2 + 1, around=main_message.created_at): async for msg in channel.history(limit=context_range * 2 + 1, around=main_message.created_at):
context_messages.append(msg) context_messages.append(msg)
# Sort messages by timestamp
context_messages.sort(key=lambda m: m.created_at) context_messages.sort(key=lambda m: m.created_at)
except Exception as e: except Exception as e:
logger.warning(f"Could not fetch context messages: {e}") logger.warning(f"Could not fetch context messages: {e}")
context_messages = [main_message] context_messages = [main_message]
# Process all messages (main + context)
all_messages_data = [] all_messages_data = []
for message in context_messages: for message in context_messages:
# Process attachments for this message
attachments_data = [] attachments_data = []
# NEW: download all attachments of this message once
downloaded_attachments = await download_message_attachments(
message=message,
guild_id=channel.guild.id,
subfolder=f"message_archive/{message.id}"
)
# Build a lookup by original attachment id or filename
downloaded_map = {
str(item.get("attachment_id")): item
for item in downloaded_attachments
}
for attachment in message.attachments: for attachment in message.attachments:
downloaded_file = downloaded_map.get(str(attachment.id))
attachment_info = { attachment_info = {
"filename": attachment.filename, "filename": attachment.filename,
"url": attachment.url, "url": attachment.url,
"proxy_url": attachment.proxy_url, "proxy_url": attachment.proxy_url,
"size": attachment.size, "size": attachment.size,
"content_type": attachment.content_type "content_type": attachment.content_type,
"attachment_id": str(attachment.id)
} }
# Download and encode image attachments for permanent storage # NEW: add local saved file info if download worked
if attachment.content_type and attachment.content_type.startswith('image/'): if downloaded_file:
try: attachment_info["local_path"] = downloaded_file.get("local_path")
import aiohttp attachment_info["saved_permanently"] = True
import base64 else:
attachment_info["saved_permanently"] = False
async with aiohttp.ClientSession() as session:
async with session.get(attachment.url) as response:
if response.status == 200 and len(await response.read()) < 8 * 1024 * 1024: # Max 8MB
image_data = await response.read()
attachment_info["data"] = base64.b64encode(image_data).decode('utf-8')
except Exception as e:
logger.warning(f"Could not download attachment {attachment.filename}: {e}")
attachments_data.append(attachment_info) attachments_data.append(attachment_info)
# Process embeds for this message
embeds_data = [] embeds_data = []
for embed in message.embeds: for embed in message.embeds:
embed_info = { embed_info = {
@@ -4833,8 +4881,7 @@ async def get_message_data(channel, message_id, context_range=3):
"timestamp": embed.timestamp.isoformat() if embed.timestamp else None "timestamp": embed.timestamp.isoformat() if embed.timestamp else None
} }
embeds_data.append(embed_info) embeds_data.append(embed_info)
# Create message data
msg_data = { msg_data = {
"id": message.id, "id": message.id,
"content": message.content, "content": message.content,
@@ -4848,21 +4895,18 @@ async def get_message_data(channel, message_id, context_range=3):
"edited_at": message.edited_at.isoformat() if message.edited_at else None, "edited_at": message.edited_at.isoformat() if message.edited_at else None,
"message_type": str(message.type), "message_type": str(message.type),
"flags": message.flags.value if message.flags else 0, "flags": message.flags.value if message.flags else 0,
"is_main_message": message.id == message_id # Mark the main referenced message "is_main_message": message.id == message_id
} }
all_messages_data.append(msg_data) all_messages_data.append(msg_data)
# Return structured data with main message and context
return { return {
"main_message": next((msg for msg in all_messages_data if msg["is_main_message"]), None), "main_message": next((msg for msg in all_messages_data if msg["is_main_message"]), None),
"context_messages": all_messages_data, "context_messages": all_messages_data,
"context_range": context_range, "context_range": context_range,
"total_messages": len(all_messages_data) "total_messages": len(all_messages_data)
} }
return message_data
except discord.NotFound: except discord.NotFound:
logger.warning(f"Message {message_id} not found") logger.warning(f"Message {message_id} not found")
return None return None
@@ -5982,30 +6026,27 @@ async def viewwarn(ctx, warning_id: int):
@client.hybrid_command() @client.hybrid_command()
async def viewmute(ctx, identifier: str): async def viewmute(ctx, identifier: str):
"""View detailed information about a specific mute (Requires Permission Level 5 or higher) """View detailed information about a specific mute (Requires Permission Level 5 or higher)
Parameters: Parameters:
- identifier: Mute ID (e.g. 123) or Process UUID (e.g. abc123def-456...) - identifier: Mute ID (e.g. 123) or Process UUID (e.g. abc123def-456...)
""" """
# Check if it's a slash command and defer if needed is_slash_command = hasattr(ctx, "interaction") and ctx.interaction
is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction
if is_slash_command: if is_slash_command:
await ctx.defer() await ctx.defer()
# Helper function for sending responses async def send_response(content=None, embed=None, ephemeral=False, files=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, files=files or [])
elif hasattr(ctx, 'response') and not ctx.response.is_done(): elif hasattr(ctx, "response") and not ctx.response.is_done():
await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, file=file) await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, files=files or [])
else: else:
await ctx.send(content=content, embed=embed, file=file) await ctx.send(content=content, embed=embed, files=files or [])
else: else:
await ctx.send(content=content, embed=embed, file=file) await ctx.send(content=content, embed=embed, files=files or [])
except Exception as e: except Exception as e:
logger.error(f"Error sending response in viewmute command: {e}") logger.error(f"Error sending response in viewmute command: {e}")
# Final fallback - try basic send
try: try:
if embed: if embed:
await ctx.send(embed=embed) await ctx.send(embed=embed)
@@ -6013,12 +6054,60 @@ async def viewmute(ctx, identifier: str):
await ctx.send(content=content) await ctx.send(content=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 also failed: {fallback_error}")
def safe_json_loads(value, default=None):
if default is None:
default = []
if not value:
return default
try:
return json.loads(value)
except Exception:
return default
def extract_attachment_records(mute_data):
attachment_records = []
# Preferred: archived full message JSON
possible_message_json_fields = [
"message_data",
"message_context",
"context_messages_json",
"message_json"
]
for field_name in possible_message_json_fields:
raw_value = mute_data.get(field_name)
parsed = safe_json_loads(raw_value, default=None)
if isinstance(parsed, dict):
context_messages = parsed.get("context_messages", [])
for msg in context_messages:
msg_attachments = safe_json_loads(msg.get("attachments"), [])
for att in msg_attachments:
att["_source_message_id"] = msg.get("id")
att["_source_author"] = msg.get("author_name")
attachment_records.append(att)
if attachment_records:
return attachment_records
# Fallback: direct attachment field
possible_attachment_fields = [
"message_attachments",
"attachments",
"attachment_data"
]
for field_name in possible_attachment_fields:
raw_value = mute_data.get(field_name)
parsed = safe_json_loads(raw_value, [])
if isinstance(parsed, list) and parsed:
attachment_records.extend(parsed)
return attachment_records
try: try:
# 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",
@@ -6028,133 +6117,33 @@ async def viewmute(ctx, identifier: str):
await send_response(embed=embed, ephemeral=True) await send_response(embed=embed, ephemeral=True)
return return
# Determine if identifier is a mute ID (numeric) or process UUID (alphanumeric)
is_mute_id = identifier.isdigit() is_mute_id = identifier.isdigit()
# Get mute details from user_mutes database (preferred) or active_processes as fallback
connection = None connection = None
cursor = None cursor = None
try: try:
connection = connect_to_database() connection = connect_to_database()
cursor = connection.cursor() cursor = connection.cursor()
# Try to find mute in user_mutes table
if is_mute_id: if is_mute_id:
# Search by mute ID
select_query = """ select_query = """
SELECT * FROM user_mutes SELECT * FROM user_mutes
WHERE id = %s AND guild_id = %s WHERE id = %s AND guild_id = %s
LIMIT 1 LIMIT 1
""" """
cursor.execute(select_query, (int(identifier), ctx.guild.id)) cursor.execute(select_query, (int(identifier), ctx.guild.id))
else: else:
# Search by process UUID
select_query = """ select_query = """
SELECT * FROM user_mutes SELECT * FROM user_mutes
WHERE process_uuid = %s AND guild_id = %s WHERE process_uuid = %s AND guild_id = %s
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 1 LIMIT 1
""" """
cursor.execute(select_query, (identifier, ctx.guild.id)) cursor.execute(select_query, (identifier, ctx.guild.id))
mute_result = cursor.fetchone() mute_result = cursor.fetchone()
if mute_result: if not mute_result:
# Found in user_mutes table
columns = [desc[0] for desc in cursor.description]
mute_data = dict(zip(columns, mute_result))
# Get user and moderator objects
muted_user = await client.fetch_user(int(mute_data['user_id']))
moderator = await client.fetch_user(int(mute_data['moderator_id']))
# Get channel
channel = ctx.guild.get_channel(int(mute_data['channel_id'])) if mute_data['channel_id'] else None
# Create detailed embed
embed = discord.Embed(
title=f"🔇 Mute Details - ID: {mute_data['id']}",
color=0xff0000,
timestamp=mute_data['created_at']
)
embed.add_field(name="👤 Muted User", value=f"{muted_user.mention}\n`{muted_user.id}`", inline=True)
embed.add_field(name="👮 Moderator", value=f"{moderator.mention}\n`{moderator.id}`", inline=True)
embed.add_field(name="📅 Muted At", value=f"<t:{int(mute_data['start_time'].timestamp())}:F>", inline=True)
# Add status information
status_emoji = {"active": "🟢", "completed": "", "expired": "", "cancelled": ""}.get(mute_data['status'], "")
aktiv_status = "🟢 Active" if mute_data['aktiv'] else "🔴 Inactive"
status_text = f"{status_emoji} **{mute_data['status'].title()}** ({aktiv_status})"
embed.add_field(name="📊 Status", value=status_text, inline=True)
# End time and duration info
if mute_data['end_time']:
if mute_data['aktiv'] and mute_data['status'] == 'active':
embed.add_field(name="⏰ Ends At", value=f"<t:{int(mute_data['end_time'].timestamp())}:F>\n<t:{int(mute_data['end_time'].timestamp())}:R>", inline=True)
else:
embed.add_field(name="⏰ Ended At", value=f"<t:{int(mute_data['end_time'].timestamp())}:F>", inline=True)
embed.add_field(name="⏱️ Duration", value=mute_data['duration'], inline=True)
# Add reason
embed.add_field(name="📝 Reason", value=mute_data['reason'], inline=False)
# Add channel information
if channel:
embed.add_field(name="📍 Channel", value=f"{channel.mention}\n`{channel.id}`", inline=True)
# Add mute role information
if mute_data['mute_role_id']:
mute_role = ctx.guild.get_role(int(mute_data['mute_role_id']))
if mute_role:
embed.add_field(name="🎭 Mute Role", value=f"{mute_role.mention}\n`{mute_role.id}`", inline=True)
else:
embed.add_field(name="🎭 Mute Role", value=f"❌ Deleted Role\n`{mute_data['mute_role_id']}`", inline=True)
# Unmute information
if not mute_data['aktiv'] and mute_data['unmuted_at']:
unmute_info = f"<t:{int(mute_data['unmuted_at'].timestamp())}:F>"
if mute_data['unmuted_by']:
unmuter = await client.fetch_user(int(mute_data['unmuted_by']))
unmute_info += f"\nBy: {unmuter.mention}"
if mute_data['auto_unmuted']:
unmute_info += "\n🤖 Automatic unmute"
embed.add_field(name="🔓 Unmuted At", value=unmute_info, inline=True)
# Message reference if available
if mute_data['message_id'] and mute_data['message_content']:
content_preview = mute_data['message_content'][:100] + "..." if len(mute_data['message_content']) > 100 else mute_data['message_content']
embed.add_field(name="📄 Referenced Message", value=f"ID: `{mute_data['message_id']}`\nContent: {content_preview}", inline=False)
embed.add_field(name="🆔 Process UUID", value=f"`{mute_data['process_uuid']}`", inline=False)
embed.add_field(name="🆔 Mute Record ID", value=f"`{mute_data['id']}`", inline=True)
embed.set_thumbnail(url=muted_user.display_avatar.url)
embed.set_footer(text=f"Mute Record from Database | Server: {ctx.guild.name}")
await send_response(embed=embed)
return
else:
# Fallback to active_processes table (only if identifier is UUID format)
if not is_mute_id:
select_query = """
SELECT uuid, process_type, guild_id, channel_id, user_id, target_id,
created_at, end_time, status, data
FROM active_processes
WHERE uuid = %s AND guild_id = %s AND process_type = 'mute'
"""
cursor.execute(select_query, (identifier, ctx.guild.id))
result = cursor.fetchone()
if result:
# Process old format result...
# [Continue with existing fallback logic]
pass
# If no results found
embed = discord.Embed( embed = discord.Embed(
title="❌ Mute Not Found", title="❌ Mute Not Found",
description=f"No mute with {'ID' if is_mute_id else 'UUID'} `{identifier}` found in this server.", description=f"No mute with {'ID' if is_mute_id else 'UUID'} `{identifier}` found in this server.",
@@ -6162,94 +6151,127 @@ async def viewmute(ctx, identifier: str):
) )
await send_response(embed=embed, ephemeral=True) await send_response(embed=embed, ephemeral=True)
return return
# Parse result (fallback to old format) columns = [desc[0] for desc in cursor.description]
uuid, process_type, guild_id, channel_id, user_id, target_id, created_at, end_time, status, data = result mute_data = dict(zip(columns, mute_result))
# Parse data JSON muted_user = await client.fetch_user(int(mute_data["user_id"]))
import json moderator = await client.fetch_user(int(mute_data["moderator_id"]))
proc_data = json.loads(data) if data else {} channel = ctx.guild.get_channel(int(mute_data["channel_id"])) if mute_data.get("channel_id") else None
# Get user and moderator objects
muted_user = await client.fetch_user(target_id)
moderator_id = proc_data.get('moderator_id', user_id)
moderator = await client.fetch_user(moderator_id)
# Get channel
channel = ctx.guild.get_channel(channel_id) if channel_id else None
# Create detailed embed
embed = discord.Embed( embed = discord.Embed(
title=f"🔇 Mute Details - ID: {uuid[:8]}", title=f"🔇 Mute Details - ID: {mute_data['id']}",
color=0xff0000, color=0xff0000,
timestamp=created_at timestamp=mute_data["created_at"]
) )
embed.add_field(name="👤 Muted User", value=f"{muted_user.mention}\n`{muted_user.id}`", inline=True) embed.add_field(name="👤 Muted User", value=f"{muted_user.mention}\n`{muted_user.id}`", inline=True)
embed.add_field(name="👮 Moderator", value=f"{moderator.mention}\n`{moderator_id}`", inline=True) embed.add_field(name="👮 Moderator", value=f"{moderator.mention}\n`{moderator.id}`", inline=True)
embed.add_field(name="📅 Muted At", value=f"<t:{int(created_at.timestamp())}:F>", inline=True) embed.add_field(name="📅 Muted At", value=f"<t:{int(mute_data['start_time'].timestamp())}:F>", inline=True)
# Add status and duration information status_emoji = {"active": "🟢", "completed": "", "expired": "", "cancelled": ""}.get(mute_data["status"], "")
status_emoji = {"active": "🟢", "completed": "", "expired": "", "cancelled": ""}.get(status, "") aktiv_status = "🟢 Active" if mute_data["aktiv"] else "🔴 Inactive"
status_text = f"{status_emoji} **{status.title()}**" status_text = f"{status_emoji} **{mute_data['status'].title()}** ({aktiv_status})"
embed.add_field(name="📊 Status", value=status_text, inline=True) embed.add_field(name="📊 Status", value=status_text, inline=True)
if end_time: if mute_data.get("end_time"):
if status == "active": if mute_data["aktiv"] and mute_data["status"] == "active":
embed.add_field(name="⏰ Ends At", value=f"<t:{int(end_time.timestamp())}:F>\n<t:{int(end_time.timestamp())}:R>", inline=True) embed.add_field(
name="⏰ Ends At",
value=f"<t:{int(mute_data['end_time'].timestamp())}:F>\n<t:{int(mute_data['end_time'].timestamp())}:R>",
inline=True
)
else: else:
embed.add_field(name="⏰ Ended At", value=f"<t:{int(end_time.timestamp())}:F>", inline=True) embed.add_field(
name="⏰ Ended At",
embed.add_field(name="🆔 Process UUID", value=f"`{uuid}`", inline=True) value=f"<t:{int(mute_data['end_time'].timestamp())}:F>",
inline=True
# Add reason )
reason = mute_data.get('reason', 'No reason provided')
embed.add_field(name="📝 Reason", value=reason, inline=False) embed.add_field(name="⏱️ Duration", value=mute_data["duration"], inline=True)
embed.add_field(name="📝 Reason", value=mute_data["reason"], inline=False)
# Add channel information
if channel: if channel:
embed.add_field(name="📍 Channel", value=f"{channel.mention}\n`{channel.id}`", inline=True) embed.add_field(name="📍 Channel", value=f"{channel.mention}\n`{channel.id}`", inline=True)
# Add mute role information if mute_data.get("mute_role_id"):
mute_role_id = mute_data.get('mute_role_id') mute_role = ctx.guild.get_role(int(mute_data["mute_role_id"]))
if mute_role_id:
mute_role = ctx.guild.get_role(mute_role_id)
if mute_role: if mute_role:
embed.add_field(name="🎭 Mute Role", value=f"{mute_role.mention}\n`{mute_role.id}`", inline=True) embed.add_field(name="🎭 Mute Role", value=f"{mute_role.mention}\n`{mute_role.id}`", inline=True)
else: else:
embed.add_field(name="🎭 Mute Role", value=f"❌ Deleted Role\n`{mute_role_id}`", inline=True) embed.add_field(name="🎭 Mute Role", value=f"❌ Deleted Role\n`{mute_data['mute_role_id']}`", inline=True)
# Add duration calculation if still active if not mute_data["aktiv"] and mute_data.get("unmuted_at"):
if status == "active" and end_time: unmute_info = f"<t:{int(mute_data['unmuted_at'].timestamp())}:F>"
from datetime import datetime if mute_data.get("unmuted_by"):
now = datetime.now() try:
if end_time > now: unmuter = await client.fetch_user(int(mute_data["unmuted_by"]))
duration_left = end_time - now unmute_info += f"\nBy: {unmuter.mention}"
days = duration_left.days except Exception:
hours, remainder = divmod(duration_left.seconds, 3600) unmute_info += f"\nBy: `{mute_data['unmuted_by']}`"
minutes, _ = divmod(remainder, 60) if mute_data.get("auto_unmuted"):
unmute_info += "\n🤖 Automatic unmute"
duration_text = [] embed.add_field(name="🔓 Unmuted At", value=unmute_info, inline=True)
if days > 0:
duration_text.append(f"{days}d") if mute_data.get("message_id") and mute_data.get("message_content"):
if hours > 0: content_preview = mute_data["message_content"][:100] + "..." if len(mute_data["message_content"]) > 100 else mute_data["message_content"]
duration_text.append(f"{hours}h") embed.add_field(
if minutes > 0: name="📄 Referenced Message",
duration_text.append(f"{minutes}m") value=f"ID: `{mute_data['message_id']}`\nContent: {content_preview}",
inline=False
embed.add_field(name="⏳ Time Remaining", value=" ".join(duration_text) if duration_text else "Less than 1 minute", inline=True) )
attachment_records = extract_attachment_records(mute_data)
files_to_send = []
attachment_lines = []
for att in attachment_records[:10]:
filename = att.get("filename", "unknown_file")
local_path = att.get("local_path")
size = att.get("size")
content_type = att.get("content_type", "unknown")
saved_permanently = att.get("saved_permanently", False)
size_text = f"{size} bytes" if size is not None else "unknown size"
status_text = "saved locally" if (saved_permanently and local_path) else "link only"
attachment_lines.append(f"• `{filename}` ({content_type}, {size_text}) — {status_text}")
if local_path:
try:
if os.path.exists(local_path):
files_to_send.append(discord.File(local_path, filename=os.path.basename(local_path)))
except Exception as e:
logger.warning(f"Could not attach local file {local_path}: {e}")
if attachment_lines:
preview_lines = attachment_lines[:8]
if len(attachment_lines) > 8:
preview_lines.append(f"• +{len(attachment_lines) - 8} more attachment(s)")
embed.add_field(
name="📎 Archived Attachments",
value="\n".join(preview_lines),
inline=False
)
embed.add_field(name="🆔 Process UUID", value=f"`{mute_data['process_uuid']}`", inline=False)
embed.add_field(name="🆔 Mute Record ID", value=f"`{mute_data['id']}`", inline=True)
embed.set_thumbnail(url=muted_user.display_avatar.url) embed.set_thumbnail(url=muted_user.display_avatar.url)
embed.set_footer(text=f"Process Type: {process_type.title()} | Server: {ctx.guild.name}") embed.set_footer(text=f"Mute Record from Database | Server: {ctx.guild.name}")
await send_response(embed=embed) # Discord has upload limits, so send only a few files directly
files_to_send = files_to_send[:3]
await send_response(embed=embed, files=files_to_send if files_to_send else None)
finally: finally:
if cursor: if cursor:
cursor.close() cursor.close()
if connection: if connection:
close_database_connection(connection) close_database_connection(connection)
except Exception as e: except Exception as e:
logger.error(f"Error in viewmute command: {e}") logger.error(f"Error in viewmute command: {e}")
embed = discord.Embed( embed = discord.Embed(
@@ -6257,7 +6279,7 @@ async def viewmute(ctx, identifier: str):
description="An error occurred while retrieving mute details. Please try again.", description="An error occurred while retrieving mute details. Please try again.",
color=0xff0000 color=0xff0000
) )
await send_response(embed=embed) await send_response(embed=embed, ephemeral=True)
@client.hybrid_command() @client.hybrid_command()
async def removewarn(ctx, warning_id: int): async def removewarn(ctx, warning_id: int):