modified: bot.py
This commit is contained in:
414
bot.py
414
bot.py
@@ -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 = {
|
||||||
@@ -4834,7 +4882,6 @@ async def get_message_data(channel, message_id, context_range=3):
|
|||||||
}
|
}
|
||||||
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,12 +4895,11 @@ 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,
|
||||||
@@ -4861,8 +4907,6 @@ async def get_message_data(channel, message_id, context_range=3):
|
|||||||
"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
|
||||||
@@ -5986,26 +6030,23 @@ async def viewmute(ctx, identifier: str):
|
|||||||
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)
|
||||||
@@ -6014,11 +6055,59 @@ async def viewmute(ctx, identifier: str):
|
|||||||
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,19 +6117,15 @@ 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
|
||||||
@@ -6048,7 +6133,6 @@ async def viewmute(ctx, identifier: str):
|
|||||||
"""
|
"""
|
||||||
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
|
||||||
@@ -6059,102 +6143,7 @@ async def viewmute(ctx, identifier: str):
|
|||||||
|
|
||||||
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.",
|
||||||
@@ -6163,86 +6152,119 @@ 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",
|
||||||
|
value=f"<t:{int(mute_data['end_time'].timestamp())}:F>",
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
embed.add_field(name="🆔 Process UUID", value=f"`{uuid}`", inline=True)
|
embed.add_field(name="⏱️ Duration", value=mute_data["duration"], inline=True)
|
||||||
|
embed.add_field(name="📝 Reason", value=mute_data["reason"], inline=False)
|
||||||
|
|
||||||
# Add reason
|
|
||||||
reason = mute_data.get('reason', 'No reason provided')
|
|
||||||
embed.add_field(name="📝 Reason", value=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"
|
||||||
|
embed.add_field(name="🔓 Unmuted At", value=unmute_info, inline=True)
|
||||||
|
|
||||||
duration_text = []
|
if mute_data.get("message_id") and mute_data.get("message_content"):
|
||||||
if days > 0:
|
content_preview = mute_data["message_content"][:100] + "..." if len(mute_data["message_content"]) > 100 else mute_data["message_content"]
|
||||||
duration_text.append(f"{days}d")
|
embed.add_field(
|
||||||
if hours > 0:
|
name="📄 Referenced Message",
|
||||||
duration_text.append(f"{hours}h")
|
value=f"ID: `{mute_data['message_id']}`\nContent: {content_preview}",
|
||||||
if minutes > 0:
|
inline=False
|
||||||
duration_text.append(f"{minutes}m")
|
)
|
||||||
|
|
||||||
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:
|
||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user