modified: bot.py
This commit is contained in:
488
bot.py
488
bot.py
@@ -33,6 +33,8 @@ from urllib.parse import urlparse
|
||||
|
||||
load_dotenv()
|
||||
|
||||
ATTACHMENT_BASE_PATH = "attachments"
|
||||
|
||||
DB_HOST = os.getenv("DB_HOST")
|
||||
DB_PORT = os.getenv("DB_PORT")
|
||||
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
|
||||
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):
|
||||
@@ -4775,54 +4820,57 @@ async def reactivate_warning(warning_id):
|
||||
async def get_message_data(channel, message_id, context_range=3):
|
||||
"""Retrieves and processes message data for warning documentation with context messages"""
|
||||
try:
|
||||
# Get the main message
|
||||
main_message = await channel.fetch_message(message_id)
|
||||
|
||||
# Get context messages (before and after)
|
||||
|
||||
context_messages = []
|
||||
try:
|
||||
# Get messages around the target message
|
||||
async for msg in channel.history(limit=context_range * 2 + 1, around=main_message.created_at):
|
||||
context_messages.append(msg)
|
||||
|
||||
# Sort messages by timestamp
|
||||
|
||||
context_messages.sort(key=lambda m: m.created_at)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not fetch context messages: {e}")
|
||||
context_messages = [main_message]
|
||||
|
||||
# Process all messages (main + context)
|
||||
|
||||
all_messages_data = []
|
||||
|
||||
|
||||
for message in context_messages:
|
||||
# Process attachments for this message
|
||||
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:
|
||||
downloaded_file = downloaded_map.get(str(attachment.id))
|
||||
|
||||
attachment_info = {
|
||||
"filename": attachment.filename,
|
||||
"url": attachment.url,
|
||||
"proxy_url": attachment.proxy_url,
|
||||
"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
|
||||
if attachment.content_type and attachment.content_type.startswith('image/'):
|
||||
try:
|
||||
import aiohttp
|
||||
import base64
|
||||
|
||||
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}")
|
||||
|
||||
|
||||
# NEW: add local saved file info if download worked
|
||||
if downloaded_file:
|
||||
attachment_info["local_path"] = downloaded_file.get("local_path")
|
||||
attachment_info["saved_permanently"] = True
|
||||
else:
|
||||
attachment_info["saved_permanently"] = False
|
||||
|
||||
attachments_data.append(attachment_info)
|
||||
|
||||
# Process embeds for this message
|
||||
|
||||
embeds_data = []
|
||||
for embed in message.embeds:
|
||||
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
|
||||
}
|
||||
embeds_data.append(embed_info)
|
||||
|
||||
# Create message data
|
||||
|
||||
msg_data = {
|
||||
"id": message.id,
|
||||
"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,
|
||||
"message_type": str(message.type),
|
||||
"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)
|
||||
|
||||
# Return structured data with main message and context
|
||||
|
||||
return {
|
||||
"main_message": next((msg for msg in all_messages_data if msg["is_main_message"]), None),
|
||||
"context_messages": all_messages_data,
|
||||
"context_range": context_range,
|
||||
"total_messages": len(all_messages_data)
|
||||
}
|
||||
|
||||
return message_data
|
||||
|
||||
|
||||
except discord.NotFound:
|
||||
logger.warning(f"Message {message_id} not found")
|
||||
return None
|
||||
@@ -5982,30 +6026,27 @@ async def viewwarn(ctx, warning_id: int):
|
||||
@client.hybrid_command()
|
||||
async def viewmute(ctx, identifier: str):
|
||||
"""View detailed information about a specific mute (Requires Permission Level 5 or higher)
|
||||
|
||||
|
||||
Parameters:
|
||||
- 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:
|
||||
await ctx.defer()
|
||||
|
||||
# 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, files=None):
|
||||
try:
|
||||
if is_slash_command:
|
||||
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)
|
||||
if hasattr(ctx, "followup") and ctx.followup:
|
||||
await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, files=files or [])
|
||||
elif hasattr(ctx, "response") and not ctx.response.is_done():
|
||||
await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, files=files or [])
|
||||
else:
|
||||
await ctx.send(content=content, embed=embed, file=file)
|
||||
await ctx.send(content=content, embed=embed, files=files or [])
|
||||
else:
|
||||
await ctx.send(content=content, embed=embed, file=file)
|
||||
await ctx.send(content=content, embed=embed, files=files or [])
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending response in viewmute command: {e}")
|
||||
# Final fallback - try basic send
|
||||
try:
|
||||
if embed:
|
||||
await ctx.send(embed=embed)
|
||||
@@ -6013,12 +6054,60 @@ async def viewmute(ctx, identifier: str):
|
||||
await ctx.send(content=content)
|
||||
except Exception as 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:
|
||||
# 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",
|
||||
@@ -6028,133 +6117,33 @@ async def viewmute(ctx, identifier: str):
|
||||
await send_response(embed=embed, ephemeral=True)
|
||||
return
|
||||
|
||||
# Determine if identifier is a mute ID (numeric) or process UUID (alphanumeric)
|
||||
is_mute_id = identifier.isdigit()
|
||||
|
||||
# Get mute details from user_mutes database (preferred) or active_processes as fallback
|
||||
|
||||
connection = None
|
||||
cursor = None
|
||||
try:
|
||||
connection = connect_to_database()
|
||||
cursor = connection.cursor()
|
||||
|
||||
# Try to find mute in user_mutes table
|
||||
|
||||
if is_mute_id:
|
||||
# Search by mute ID
|
||||
select_query = """
|
||||
SELECT * FROM user_mutes
|
||||
SELECT * FROM user_mutes
|
||||
WHERE id = %s AND guild_id = %s
|
||||
LIMIT 1
|
||||
"""
|
||||
cursor.execute(select_query, (int(identifier), ctx.guild.id))
|
||||
else:
|
||||
# Search by process UUID
|
||||
select_query = """
|
||||
SELECT * FROM user_mutes
|
||||
SELECT * FROM user_mutes
|
||||
WHERE process_uuid = %s AND guild_id = %s
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
cursor.execute(select_query, (identifier, ctx.guild.id))
|
||||
|
||||
|
||||
mute_result = cursor.fetchone()
|
||||
|
||||
if 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
|
||||
|
||||
if not mute_result:
|
||||
embed = discord.Embed(
|
||||
title="❌ Mute Not Found",
|
||||
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)
|
||||
return
|
||||
|
||||
# Parse result (fallback to old format)
|
||||
uuid, process_type, guild_id, channel_id, user_id, target_id, created_at, end_time, status, data = result
|
||||
|
||||
# Parse data JSON
|
||||
import json
|
||||
proc_data = json.loads(data) if data else {}
|
||||
|
||||
# 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
|
||||
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
mute_data = dict(zip(columns, mute_result))
|
||||
|
||||
muted_user = await client.fetch_user(int(mute_data["user_id"]))
|
||||
moderator = await client.fetch_user(int(mute_data["moderator_id"]))
|
||||
channel = ctx.guild.get_channel(int(mute_data["channel_id"])) if mute_data.get("channel_id") else None
|
||||
|
||||
embed = discord.Embed(
|
||||
title=f"🔇 Mute Details - ID: {uuid[:8]}",
|
||||
title=f"🔇 Mute Details - ID: {mute_data['id']}",
|
||||
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="👮 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)
|
||||
|
||||
# Add status and duration information
|
||||
status_emoji = {"active": "🟢", "completed": "✅", "expired": "⏰", "cancelled": "❌"}.get(status, "❓")
|
||||
status_text = f"{status_emoji} **{status.title()}**"
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
if end_time:
|
||||
if 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)
|
||||
|
||||
if mute_data.get("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(end_time.timestamp())}:F>", inline=True)
|
||||
|
||||
embed.add_field(name="🆔 Process UUID", value=f"`{uuid}`", inline=True)
|
||||
|
||||
# Add reason
|
||||
reason = mute_data.get('reason', 'No reason provided')
|
||||
embed.add_field(name="📝 Reason", value=reason, inline=False)
|
||||
|
||||
# Add channel information
|
||||
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)
|
||||
embed.add_field(name="📝 Reason", value=mute_data["reason"], inline=False)
|
||||
|
||||
if channel:
|
||||
embed.add_field(name="📍 Channel", value=f"{channel.mention}\n`{channel.id}`", inline=True)
|
||||
|
||||
# Add mute role information
|
||||
mute_role_id = mute_data.get('mute_role_id')
|
||||
if mute_role_id:
|
||||
mute_role = ctx.guild.get_role(mute_role_id)
|
||||
|
||||
if mute_data.get("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_role_id}`", inline=True)
|
||||
|
||||
# Add duration calculation if still active
|
||||
if status == "active" and end_time:
|
||||
from datetime import datetime
|
||||
now = datetime.now()
|
||||
if end_time > now:
|
||||
duration_left = end_time - now
|
||||
days = duration_left.days
|
||||
hours, remainder = divmod(duration_left.seconds, 3600)
|
||||
minutes, _ = divmod(remainder, 60)
|
||||
|
||||
duration_text = []
|
||||
if days > 0:
|
||||
duration_text.append(f"{days}d")
|
||||
if hours > 0:
|
||||
duration_text.append(f"{hours}h")
|
||||
if minutes > 0:
|
||||
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)
|
||||
|
||||
embed.add_field(name="🎭 Mute Role", value=f"❌ Deleted Role\n`{mute_data['mute_role_id']}`", inline=True)
|
||||
|
||||
if not mute_data["aktiv"] and mute_data.get("unmuted_at"):
|
||||
unmute_info = f"<t:{int(mute_data['unmuted_at'].timestamp())}:F>"
|
||||
if mute_data.get("unmuted_by"):
|
||||
try:
|
||||
unmuter = await client.fetch_user(int(mute_data["unmuted_by"]))
|
||||
unmute_info += f"\nBy: {unmuter.mention}"
|
||||
except Exception:
|
||||
unmute_info += f"\nBy: `{mute_data['unmuted_by']}`"
|
||||
if mute_data.get("auto_unmuted"):
|
||||
unmute_info += "\n🤖 Automatic unmute"
|
||||
embed.add_field(name="🔓 Unmuted At", value=unmute_info, inline=True)
|
||||
|
||||
if mute_data.get("message_id") and mute_data.get("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
|
||||
)
|
||||
|
||||
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_footer(text=f"Process Type: {process_type.title()} | Server: {ctx.guild.name}")
|
||||
|
||||
await send_response(embed=embed)
|
||||
|
||||
embed.set_footer(text=f"Mute Record from Database | Server: {ctx.guild.name}")
|
||||
|
||||
# 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:
|
||||
if cursor:
|
||||
cursor.close()
|
||||
if connection:
|
||||
close_database_connection(connection)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in viewmute command: {e}")
|
||||
embed = discord.Embed(
|
||||
@@ -6257,7 +6279,7 @@ async def viewmute(ctx, identifier: str):
|
||||
description="An error occurred while retrieving mute details. Please try again.",
|
||||
color=0xff0000
|
||||
)
|
||||
await send_response(embed=embed)
|
||||
await send_response(embed=embed, ephemeral=True)
|
||||
|
||||
@client.hybrid_command()
|
||||
async def removewarn(ctx, warning_id: int):
|
||||
|
||||
Reference in New Issue
Block a user