modified: bot.py

This commit is contained in:
SimolZimol
2026-06-24 11:48:14 +02:00
parent ac301f3370
commit db18516193

388
bot.py
View File

@@ -6074,20 +6074,20 @@ 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...)
""" """
is_slash_command = hasattr(ctx, "interaction") and ctx.interaction is not None is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction
if is_slash_command: if is_slash_command:
await ctx.defer() await ctx.defer()
async def send_response(content=None, embed=None, ephemeral=False, file=None, files=None): async def send_response(content=None, embed=None, ephemeral=False, file=None, files=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, files=files or []) await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file, 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, files=files or []) await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, file=file, files=files or [])
else: else:
await ctx.send(content=content, embed=embed, file=file, files=files or []) await ctx.send(content=content, embed=embed, file=file, files=files or [])
@@ -6103,42 +6103,41 @@ 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}")
async def build_attachment_field(attachments_json: str) -> tuple[str, list[discord.File]]: def build_attachment_field(attachments_json: str) -> tuple[str, list[discord.File]]:
""" """
Parses attachment JSON, loads local files, returns (field_value_text, discord_files). Parses stored attachments JSON, loads local files.
Returns (field_value_text, list_of_discord_Files).
""" """
field_text = "" local_files = []
discord_files = [] field_lines = []
try:
atts = json.loads(attachments_json) if isinstance(attachments_json, str) else attachments_json
if not atts:
return field_text, discord_files
for att in atts: try:
filename = att.get("filename", "Unknown") attachments = json.loads(attachments_json) if isinstance(attachments_json, str) else attachments_json
for att in attachments:
local_path = att.get("local_path") local_path = att.get("local_path")
filename = att.get("filename", "unknown_file")
size = att.get("size", 0) size = att.get("size", 0)
size_kb = round(size / 1024, 1) if size else "?" size_str = f"{size / 1024:.1f} KB" if size else "?"
content_type = att.get("content_type", "unknown")
if local_path and os.path.exists(local_path): if local_path and os.path.exists(local_path):
field_text += f"• ✅ `{filename}` ({size_kb} KB, {content_type})\n" try:
discord_files.append(discord.File(local_path, filename=filename)) local_files.append(discord.File(local_path, filename=filename))
field_lines.append(f"✅ `{filename}` ({size_str}) — *locally archived*")
except Exception as e:
logger.error(f"Could not load local file {local_path}: {e}")
field_lines.append(f"⚠️ `{filename}` ({size_str}) — *file unreadable*")
else: else:
# File missing locally, try original URL as fallback info
original_url = att.get("original_url", "") original_url = att.get("original_url", "")
stored = att.get("stored", False) if original_url:
if stored: field_lines.append(f"❌ `{filename}` ({size_str}) — *local file missing* ([original URL]({original_url}) may be expired)")
field_text += f"• ❌ `{filename}` — locally saved file missing\n"
elif original_url:
field_text += f"• ⚠️ `{filename}` — [original CDN link]({original_url}) (may be expired)\n"
else: else:
field_text += f"• ❓ `{filename}` — not available\n" field_lines.append(f" `{filename}` ({size_str}) — *local file missing, no URL*")
except Exception as e: except Exception as e:
logger.error(f"Error building attachment field: {e}") logger.error(f"Error parsing attachments JSON: {e}")
field_text = "⚠️ Error loading attachment data." field_lines.append("⚠️ Could not parse attachment data.")
return field_text.strip(), discord_files return "\n".join(field_lines) if field_lines else None, local_files
try: try:
mod_data = await load_user_data(ctx.author.id, ctx.guild.id) mod_data = await load_user_data(ctx.author.id, ctx.guild.id)
@@ -6160,19 +6159,21 @@ async def viewmute(ctx, identifier: str):
connection = connect_to_database() connection = connect_to_database()
cursor = connection.cursor() cursor = connection.cursor()
# ── Primary lookup: user_mutes table ──────────────────────────────
if is_mute_id: if is_mute_id:
cursor.execute( select_query = """
"SELECT * FROM user_mutes WHERE id = %s AND guild_id = %s LIMIT 1", SELECT * FROM user_mutes
(int(identifier), ctx.guild.id) WHERE id = %s AND guild_id = %s
) LIMIT 1
"""
cursor.execute(select_query, (int(identifier), ctx.guild.id))
else: else:
cursor.execute( 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 LIMIT 1""", ORDER BY created_at DESC
(identifier, ctx.guild.id) LIMIT 1
) """
cursor.execute(select_query, (identifier, ctx.guild.id))
mute_result = cursor.fetchone() mute_result = cursor.fetchone()
@@ -6180,26 +6181,26 @@ async def viewmute(ctx, identifier: str):
columns = [desc[0] for desc in cursor.description] columns = [desc[0] for desc in cursor.description]
mute_data = dict(zip(columns, mute_result)) mute_data = dict(zip(columns, mute_result))
muted_user = await client.fetch_user(int(mute_data["user_id"])) muted_user = await client.fetch_user(int(mute_data['user_id']))
moderator = await client.fetch_user(int(mute_data["moderator_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 channel = ctx.guild.get_channel(int(mute_data['channel_id'])) if mute_data['channel_id'] else None
embed = discord.Embed( embed = discord.Embed(
title=f"🔇 Mute Details — ID: {mute_data['id']}", title=f"🔇 Mute Details — ID: {mute_data['id']}",
color=0xff0000, color=0xff0000,
timestamp=mute_data["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(mute_data['start_time'].timestamp())}:F>", 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"], "") status_emoji = {"active": "🟢", "completed": "", "expired": "", "cancelled": ""}.get(mute_data['status'], "")
aktiv_status = "🟢 Active" if mute_data["aktiv"] else "🔴 Inactive" aktiv_status = "🟢 Active" if mute_data['aktiv'] else "🔴 Inactive"
embed.add_field(name="📊 Status", value=f"{status_emoji} **{mute_data['status'].title()}** ({aktiv_status})", inline=True) embed.add_field(name="📊 Status", value=f"{status_emoji} **{mute_data['status'].title()}** ({aktiv_status})", inline=True)
if mute_data.get("end_time"): if mute_data['end_time']:
if mute_data["aktiv"] and mute_data["status"] == "active": if mute_data['aktiv'] and mute_data['status'] == 'active':
embed.add_field( embed.add_field(
name="⏰ Ends At", name="⏰ Ends At",
value=f"<t:{int(mute_data['end_time'].timestamp())}:F>\n<t:{int(mute_data['end_time'].timestamp())}:R>", value=f"<t:{int(mute_data['end_time'].timestamp())}:F>\n<t:{int(mute_data['end_time'].timestamp())}:R>",
@@ -6208,202 +6209,213 @@ async def viewmute(ctx, identifier: str):
else: else:
embed.add_field(name="⏰ Ended At", value=f"<t:{int(mute_data['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="⏱️ Duration", value=mute_data["duration"], inline=True) embed.add_field(name="⏱️ Duration", value=mute_data['duration'], inline=True)
# Time remaining (only when active) # Time remaining if active
if mute_data["aktiv"] and mute_data["status"] == "active" and mute_data.get("end_time"): if mute_data['aktiv'] and mute_data['status'] == 'active' and mute_data['end_time']:
now = datetime.now() now = datetime.now()
if mute_data["end_time"] > now: if mute_data['end_time'] > now:
delta = mute_data["end_time"] - now time_left = mute_data['end_time'] - now
days = delta.days days = time_left.days
hours, remainder = divmod(delta.seconds, 3600) hours, remainder = divmod(time_left.seconds, 3600)
minutes, _ = divmod(remainder, 60) minutes, _ = divmod(remainder, 60)
parts = [] parts = []
if days: parts.append(f"{days}d") if days > 0:
if hours: parts.append(f"{hours}h") parts.append(f"{days}d")
if minutes: parts.append(f"{minutes}m") if hours > 0:
parts.append(f"{hours}h")
if minutes > 0:
parts.append(f"{minutes}m")
embed.add_field( embed.add_field(
name="⏳ Time Remaining", name="⏳ Time Remaining",
value=" ".join(parts) if parts else "Less than 1 minute", value=" ".join(parts) if parts else "Less than 1 minute",
inline=True inline=True
) )
embed.add_field(name="📝 Reason", value=mute_data.get("reason") or "No reason provided", inline=False) embed.add_field(name="📝 Reason", value=mute_data['reason'] or "No reason provided", inline=False)
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)
if mute_data.get("mute_role_id"): if mute_data['mute_role_id']:
mute_role = ctx.guild.get_role(int(mute_data["mute_role_id"])) mute_role = ctx.guild.get_role(int(mute_data['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_data['mute_role_id']}`", 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"): if not mute_data['aktiv'] and mute_data['unmuted_at']:
unmute_info = f"<t:{int(mute_data['unmuted_at'].timestamp())}:F>" unmute_info = f"<t:{int(mute_data['unmuted_at'].timestamp())}:F>"
if mute_data.get("unmuted_by"): if mute_data['unmuted_by']:
unmuter = await client.fetch_user(int(mute_data["unmuted_by"])) unmuter = await client.fetch_user(int(mute_data['unmuted_by']))
unmute_info += f"\nBy: {unmuter.mention}" unmute_info += f"\nBy: {unmuter.mention}"
if mute_data.get("auto_unmuted"): if mute_data['auto_unmuted']:
unmute_info += "\n🤖 Automatic unmute" unmute_info += "\n🤖 Automatic unmute"
embed.add_field(name="🔓 Unmuted At", value=unmute_info, inline=True) embed.add_field(name="🔓 Unmuted At", value=unmute_info, inline=True)
# ── Referenced message (from message_data JSON in DB) ────────── # ── Referenced message + locally archived attachments ──────────
raw_msg_data = mute_data.get("message_data") local_files = []
local_files = []
if raw_msg_data: raw_message_data = mute_data.get('message_data')
if raw_message_data:
try: try:
msg_data = json.loads(raw_msg_data) if isinstance(raw_msg_data, str) else raw_msg_data msg_data = json.loads(raw_message_data) if isinstance(raw_message_data, str) else raw_message_data
# Determine which format (new context dict or flat dict) # Determine which format the stored message_data uses
if isinstance(msg_data, dict) and "main_message" in msg_data: if isinstance(msg_data, dict) and "main_message" in msg_data:
main_msg = msg_data.get("main_message") or {} main_msg = msg_data.get("main_message") or {}
atts_json = main_msg.get("attachments", "[]") attachments_json = main_msg.get("attachments")
msg_id = main_msg.get("id", mute_data.get("message_id", "?"))
msg_ch_id = main_msg.get("channel_id", "?") # Build referenced message field
msg_author = main_msg.get("author_name", "Unknown") msg_info = f"**Message ID:** `{main_msg.get('id', 'N/A')}`\n"
msg_content = main_msg.get("content", "") msg_info += f"**Channel:** <#{main_msg.get('channel_id', 'N/A')}>\n"
msg_info += f"**Author:** {main_msg.get('author_name', 'Unknown')}\n"
if main_msg.get("content"):
preview = main_msg["content"][:150] + "..." if len(main_msg["content"]) > 150 else main_msg["content"]
msg_info += f"**Content:** {preview}"
embed.add_field(name="📄 Referenced Message", value=msg_info, inline=False)
else: else:
atts_json = msg_data.get("attachments", "[]") # Flat format (honeypot / direct archive)
msg_id = msg_data.get("id", mute_data.get("message_id", "?")) attachments_json = msg_data.get("attachments")
msg_ch_id = msg_data.get("channel_id", "?")
msg_author = msg_data.get("author_name") or f"<@{msg_data.get('author_id', '?')}>"
msg_content = msg_data.get("content", "")
msg_info = f"**Message ID:** `{msg_id}`\n" msg_info = f"**Message ID:** `{msg_data.get('id', 'N/A')}`\n"
msg_info += f"**Channel:** <#{msg_ch_id}>\n" msg_info += f"**Channel:** <#{msg_data.get('channel_id', 'N/A')}>\n"
msg_info += f"**Author:** {msg_author}\n" author_id = msg_data.get("author_id")
if msg_content: author_name = msg_data.get("author_name", "Unknown")
preview = msg_content[:150] + "..." if len(msg_content) > 150 else msg_content msg_info += f"**Author:** {f'<@{author_id}>' if author_id else author_name}\n"
msg_info += f"**Content:** {preview}" if msg_data.get("content"):
preview = msg_data["content"][:150] + "..." if len(msg_data["content"]) > 150 else msg_data["content"]
msg_info += f"**Content:** {preview}"
embed.add_field(name="📄 Referenced Message", value=msg_info, inline=False)
embed.add_field(name="📄 Referenced Message", value=msg_info, inline=False) # Load local attachment files
if attachments_json:
# ── Attachment section ───────────────────────────────── att_field_text, local_files = build_attachment_field(attachments_json)
if atts_json and atts_json != "[]":
att_field_text, local_files = await build_attachment_field(atts_json)
if att_field_text: if att_field_text:
stored_count = len(local_files) embed.add_field(
total_count = len(json.loads(atts_json) if isinstance(atts_json, str) else atts_json) name=f"📎 Archived Attachments ({len(local_files)} file(s) attached)" if local_files else "📎 Archived Attachments",
field_title = f"📎 Archived Attachments ({stored_count}/{total_count} available locally)" value=att_field_text,
embed.add_field(name=field_title, value=att_field_text, inline=False) inline=False
)
except Exception as e: except Exception as e:
logger.error(f"Error parsing message_data in viewmute: {e}") logger.error(f"Error parsing message_data for viewmute {identifier}: {e}")
embed.add_field(name="📄 Referenced Message", value="⚠️ Error loading message data.", inline=False) embed.add_field(name="📄 Referenced Message", value="⚠️ Could not parse archived message data.", inline=False)
# Fallback: if no message_data but message_id + message_content columns exist elif mute_data.get('message_id') and mute_data.get('message_content'):
elif mute_data.get("message_id") and mute_data.get("message_content"): # Legacy fallback: separate columns (no attachment support)
preview = mute_data["message_content"][:100] + "..." if len(mute_data["message_content"]) > 100 else 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( embed.add_field(
name="📄 Referenced Message", name="📄 Referenced Message",
value=f"ID: `{mute_data['message_id']}`\nContent: {preview}", value=f"ID: `{mute_data['message_id']}`\nContent: {content_preview}",
inline=False inline=False
) )
# ────────────────────────────────────────────────────────────────
embed.add_field(name="🆔 Process UUID", value=f"`{mute_data['process_uuid']}`", 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.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"Mute Record from Database | Server: {ctx.guild.name}") embed.set_footer(text=f"Mute Record from Database | Server: {ctx.guild.name}")
# Send embed + local files (if any)
if local_files: if local_files:
# Send embed + files together
await send_response(embed=embed, files=local_files[:10]) await send_response(embed=embed, files=local_files[:10])
if len(local_files) > 10:
# Discord allows max 10 files per message
await send_response(
content=f"📎 +{len(local_files) - 10} more attachment(s) exceeded the Discord file limit.",
ephemeral=True
)
else: else:
await send_response(embed=embed) await send_response(embed=embed)
return return
# ── Fallback lookup: active_processes table ──────────────────────── else:
if not is_mute_id: # Fallback: active_processes table (UUID only)
cursor.execute( if not is_mute_id:
"""SELECT uuid, process_type, guild_id, channel_id, user_id, target_id, select_query = """
created_at, end_time, status, data SELECT uuid, process_type, guild_id, channel_id, user_id, target_id,
FROM active_processes created_at, end_time, status, data
WHERE uuid = %s AND guild_id = %s AND process_type = 'mute'""", FROM active_processes
(identifier, ctx.guild.id) WHERE uuid = %s AND guild_id = %s AND process_type = 'mute'
) """
result = cursor.fetchone() cursor.execute(select_query, (identifier, ctx.guild.id))
result = cursor.fetchone()
if result: if result:
uuid, process_type, guild_id, channel_id, user_id, target_id, created_at, end_time, status, data = result uuid, process_type, guild_id, channel_id, user_id, target_id, created_at, end_time, status, data = result
proc_data = json.loads(data) if data else {} proc_data = json.loads(data) if data else {}
muted_user = await client.fetch_user(target_id)
moderator_id = proc_data.get("moderator_id", user_id)
moderator = await client.fetch_user(moderator_id)
channel = ctx.guild.get_channel(channel_id) if channel_id else None
embed = discord.Embed( muted_user = await client.fetch_user(target_id)
title=f"🔇 Mute Details — UUID: {str(uuid)[:8]}", moderator_id = proc_data.get('moderator_id', user_id)
color=0xff0000, moderator = await client.fetch_user(moderator_id)
timestamp=created_at channel = ctx.guild.get_channel(channel_id) if channel_id else None
)
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)
status_emoji = {"active": "🟢", "completed": "", "expired": "", "cancelled": ""}.get(status, "") embed = discord.Embed(
embed.add_field(name="📊 Status", value=f"{status_emoji} **{status.title()}**", inline=True) title=f"🔇 Mute Details — UUID: {str(uuid)[:8]}",
color=0xff0000,
if end_time: timestamp=created_at
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
)
else:
embed.add_field(name="⏰ Ended At", value=f"<t:{int(end_time.timestamp())}:F>", inline=True)
if status == "active" and end_time and end_time > datetime.now():
delta = end_time - datetime.now()
days = delta.days
hours, remainder = divmod(delta.seconds, 3600)
minutes, _ = divmod(remainder, 60)
parts = []
if days: parts.append(f"{days}d")
if hours: parts.append(f"{hours}h")
if minutes: parts.append(f"{minutes}m")
embed.add_field(
name="⏳ Time Remaining",
value=" ".join(parts) if parts else "Less than 1 minute",
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="📅 Muted At", value=f"<t:{int(created_at.timestamp())}:F>", inline=True)
embed.add_field(name="📝 Reason", value=proc_data.get("reason") or "No reason provided", inline=False) status_emoji = {"active": "🟢", "completed": "", "expired": "", "cancelled": ""}.get(status, "")
embed.add_field(name="📊 Status", value=f"{status_emoji} **{status.title()}**", inline=True)
if channel: if end_time:
embed.add_field(name="📍 Channel", value=f"{channel.mention}\n`{channel.id}`", inline=True) 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
)
else:
embed.add_field(name="⏰ Ended At", value=f"<t:{int(end_time.timestamp())}:F>", inline=True)
mute_role_id = proc_data.get("mute_role_id") reason = proc_data.get('reason', 'No reason provided')
if mute_role_id: embed.add_field(name="📝 Reason", value=reason, inline=False)
mute_role = ctx.guild.get_role(int(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)
embed.add_field(name="🆔 Process UUID", value=f"`{uuid}`", inline=False) if channel:
embed.set_thumbnail(url=muted_user.display_avatar.url) embed.add_field(name="📍 Channel", value=f"{channel.mention}\n`{channel.id}`", inline=True)
embed.set_footer(text=f"Process Type: {process_type.title()} | Server: {ctx.guild.name}")
await send_response(embed=embed) mute_role_id = proc_data.get('mute_role_id')
return if mute_role_id:
mute_role = ctx.guild.get_role(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)
# ── Nothing found ────────────────────────────────────────────────── if status == "active" and end_time:
embed = discord.Embed( now = datetime.now()
title="❌ Mute Not Found", if end_time > now:
description=f"No mute with {'ID' if is_mute_id else 'UUID'} `{identifier}` found in this server.", time_left = end_time - now
color=0xff0000 days = time_left.days
) hours, remainder = divmod(time_left.seconds, 3600)
await send_response(embed=embed, ephemeral=True) minutes, _ = divmod(remainder, 60)
parts = []
if days > 0:
parts.append(f"{days}d")
if hours > 0:
parts.append(f"{hours}h")
if minutes > 0:
parts.append(f"{minutes}m")
embed.add_field(
name="⏳ Time Remaining",
value=" ".join(parts) if parts else "Less than 1 minute",
inline=True
)
embed.add_field(name="🆔 Process UUID", value=f"`{uuid}`", inline=False)
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)
return
embed = discord.Embed(
title="❌ Mute Not Found",
description=f"No mute with {'ID' if is_mute_id else 'UUID'} `{identifier}` found in this server.",
color=0xff0000
)
await send_response(embed=embed, ephemeral=True)
return
finally: finally:
if cursor: if cursor:
@@ -6418,7 +6430,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, ephemeral=True) await send_response(embed=embed)
@client.hybrid_command() @client.hybrid_command()
async def removewarn(ctx, warning_id: int): async def removewarn(ctx, warning_id: int):