Applying previous commit.
This commit is contained in:
parent
dfcab5eb6d
commit
9f039a0b65
@ -1,5 +1,6 @@
|
||||
import discord
|
||||
from discord.ext import commands, tasks
|
||||
from discord import ui
|
||||
import datetime
|
||||
import asyncio
|
||||
import aiohttp # Added for webhook sending
|
||||
@ -54,6 +55,47 @@ class LoggingCog(commands.Cog):
|
||||
else:
|
||||
asyncio.create_task(self.start_audit_log_poller_when_ready()) # Keep this for initial start
|
||||
|
||||
class LogView(ui.LayoutView):
|
||||
"""Simple view for log messages."""
|
||||
|
||||
def __init__(self, bot: commands.Bot, title: str, description: str,
|
||||
color: discord.Color, author: Optional[discord.abc.User],
|
||||
footer: Optional[str]):
|
||||
super().__init__(timeout=None)
|
||||
self.container = ui.Container(accent_colour=color)
|
||||
self.add_item(self.container)
|
||||
|
||||
self.header = ui.Section(accessory=(ui.Thumbnail(media=author.display_avatar.url)
|
||||
if author else None))
|
||||
self.header.add_item(ui.TextDisplay(f"**{title}**"))
|
||||
if description:
|
||||
self.header.add_item(ui.TextDisplay(description))
|
||||
self.container.add_item(self.header)
|
||||
|
||||
self.fields_container = ui.Container()
|
||||
self.container.add_item(self.fields_container)
|
||||
|
||||
self.container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
|
||||
|
||||
footer_text = footer or f"Bot ID: {bot.user.id}" + (
|
||||
f" | User ID: {author.id}" if author else "")
|
||||
self.footer_display = ui.TextDisplay(footer_text)
|
||||
self.container.add_item(self.footer_display)
|
||||
|
||||
def add_field(self, *, name: str, value: str, inline: bool = False) -> None:
|
||||
"""Mimic discord.Embed.add_field for compatibility."""
|
||||
self.fields_container.add_item(ui.TextDisplay(f"**{name}:** {value}"))
|
||||
|
||||
def set_author(self, *, name: str, icon_url: Optional[str] | None = None) -> None:
|
||||
"""Set the author line similarly to discord.Embed.set_author."""
|
||||
if icon_url:
|
||||
self.header.accessory = ui.Thumbnail(media=icon_url)
|
||||
self.header.add_item(ui.TextDisplay(name))
|
||||
|
||||
def set_footer(self, *, text: str) -> None:
|
||||
"""Set footer text similarly to discord.Embed.set_footer."""
|
||||
self.footer_display.content = text
|
||||
|
||||
def _user_display(self, user: Union[discord.Member, discord.User]) -> str:
|
||||
"""Return display name, username and ID string for a user."""
|
||||
display = user.display_name if isinstance(user, discord.Member) else user.name
|
||||
@ -109,8 +151,8 @@ class LoggingCog(commands.Cog):
|
||||
await self.session.close()
|
||||
log.info("aiohttp ClientSession closed for LoggingCog.")
|
||||
|
||||
async def _send_log_embed(self, guild: discord.Guild, embed: discord.Embed):
|
||||
"""Sends the log embed via the configured webhook for the guild."""
|
||||
async def _send_log_embed(self, guild: discord.Guild, embed: ui.LayoutView):
|
||||
"""Sends the log view via the configured webhook for the guild."""
|
||||
if not self.session or self.session.closed:
|
||||
log.error(f"aiohttp session not available or closed in LoggingCog for guild {guild.id}. Cannot send log.")
|
||||
return
|
||||
@ -124,9 +166,9 @@ class LoggingCog(commands.Cog):
|
||||
try:
|
||||
webhook = discord.Webhook.from_url(webhook_url, session=self.session)
|
||||
await webhook.send(
|
||||
embed=embed,
|
||||
username=f"{self.bot.user.name} Logs", # Optional: Customize webhook appearance
|
||||
avatar_url=self.bot.user.display_avatar.url # Optional: Use bot's avatar
|
||||
view=embed,
|
||||
username=f"{self.bot.user.name} Logs",
|
||||
avatar_url=self.bot.user.display_avatar.url,
|
||||
)
|
||||
# log.debug(f"Sent log embed via webhook for guild {guild.id}") # Can be noisy
|
||||
except ValueError:
|
||||
@ -145,26 +187,17 @@ class LoggingCog(commands.Cog):
|
||||
log.exception(f"Unexpected error sending log via webhook for guild {guild.id}: {e}")
|
||||
|
||||
|
||||
def _create_log_embed(self, title: str, description: str = "", color: discord.Color = discord.Color.blue(), author: Optional[Union[discord.User, discord.Member]] = None, footer: Optional[str] = None) -> discord.Embed:
|
||||
"""Creates a standardized log embed."""
|
||||
embed = discord.Embed(title=title, description=description, color=color, timestamp=datetime.datetime.now(datetime.timezone.utc))
|
||||
if author:
|
||||
embed.set_author(name=str(author), icon_url=author.display_avatar.url)
|
||||
if footer:
|
||||
embed.set_footer(text=footer)
|
||||
else:
|
||||
# Add User ID to footer if author is present and footer isn't custom
|
||||
user_id_str = f" | User ID: {author.id}" if author else ""
|
||||
embed.set_footer(text=f"Bot ID: {self.bot.user.id}{user_id_str}")
|
||||
return embed
|
||||
def _create_log_embed(self, title: str, description: str = "", color: discord.Color = discord.Color.blue(), author: Optional[Union[discord.User, discord.Member]] = None, footer: Optional[str] = None) -> ui.LayoutView:
|
||||
"""Creates a standardized log view."""
|
||||
return self.LogView(self.bot, title, description, color, author, footer)
|
||||
|
||||
def _add_id_footer(self, embed: discord.Embed, obj: Union[discord.Member, discord.User, discord.Role, discord.abc.GuildChannel, discord.Message, discord.Invite, None] = None, obj_id: Optional[int] = None, id_name: str = "ID"):
|
||||
"""Adds an ID to the embed footer if possible."""
|
||||
def _add_id_footer(self, embed: ui.LayoutView, obj: Union[discord.Member, discord.User, discord.Role, discord.abc.GuildChannel, discord.Message, discord.Invite, None] = None, obj_id: Optional[int] = None, id_name: str = "ID"):
|
||||
"""Adds an ID to the footer text if possible."""
|
||||
target_id = obj_id or (obj.id if obj else None)
|
||||
if target_id:
|
||||
existing_footer = embed.footer.text or ""
|
||||
if target_id and hasattr(embed, "footer_display"):
|
||||
existing_footer = embed.footer_display.content or ""
|
||||
separator = " | " if existing_footer else ""
|
||||
embed.set_footer(text=f"{existing_footer}{separator}{id_name}: {target_id}")
|
||||
embed.footer_display.content = f"{existing_footer}{separator}{id_name}: {target_id}"
|
||||
|
||||
async def _check_log_enabled(self, guild_id: int, event_key: str) -> bool:
|
||||
"""Checks if logging is enabled for a specific event key in a guild."""
|
||||
|
@ -1,6 +1,6 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from discord import app_commands, Interaction, Embed, Color, User, Member, Object
|
||||
from discord import app_commands, Interaction, Embed, Color, User, Member, Object, ui
|
||||
import asyncpg
|
||||
import logging
|
||||
from typing import Optional, Union, Dict, Any
|
||||
@ -32,6 +32,21 @@ class ModLogCog(commands.Cog):
|
||||
# Add command group to the bot's tree
|
||||
self.bot.tree.add_command(self.modlog_group)
|
||||
|
||||
class LogView(ui.LayoutView):
|
||||
"""View used for moderation log messages."""
|
||||
|
||||
def __init__(self, bot: commands.Bot, title: str, color: discord.Color, lines: list[str], footer: str):
|
||||
super().__init__(timeout=None)
|
||||
container = ui.Container(accent_colour=color)
|
||||
self.add_item(container)
|
||||
container.add_item(ui.TextDisplay(f"**{title}**"))
|
||||
container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
|
||||
for line in lines:
|
||||
container.add_item(ui.TextDisplay(line))
|
||||
container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
|
||||
self.footer_display = ui.TextDisplay(footer)
|
||||
container.add_item(self.footer_display)
|
||||
|
||||
def _format_user(self, user: Union[Member, User, Object], guild: Optional[discord.Guild] = None) -> str:
|
||||
"""Return a string with display name, username and ID for a user-like object."""
|
||||
if isinstance(user, Object):
|
||||
@ -220,8 +235,8 @@ class ModLogCog(commands.Cog):
|
||||
# Optionally update DB to remove channel ID? Or just leave it.
|
||||
return
|
||||
|
||||
# 3. Format and send embed
|
||||
embed = self._format_log_embed(
|
||||
# 3. Format and send view
|
||||
view = self._format_log_embed(
|
||||
case_id=case_id,
|
||||
moderator=moderator, # Pass the object for display formatting
|
||||
target=target,
|
||||
@ -233,7 +248,7 @@ class ModLogCog(commands.Cog):
|
||||
ai_details=ai_details,
|
||||
moderator_id_override=moderator_id_override # Pass override for formatting
|
||||
)
|
||||
log_message = await log_channel.send(embed=embed)
|
||||
log_message = await log_channel.send(view=view)
|
||||
|
||||
# 4. Update DB with message details
|
||||
await mod_log_db.update_mod_log_message_details(self.pool, case_id, log_message.id, log_channel.id)
|
||||
@ -241,6 +256,7 @@ class ModLogCog(commands.Cog):
|
||||
except Exception as e:
|
||||
log.exception(f"Error during Discord mod log message sending/updating for case {case_id} in guild {guild_id}: {e}")
|
||||
|
||||
|
||||
def _format_log_embed(
|
||||
self,
|
||||
case_id: int,
|
||||
@ -252,9 +268,9 @@ class ModLogCog(commands.Cog):
|
||||
guild: discord.Guild,
|
||||
source: str = "BOT",
|
||||
ai_details: Optional[Dict[str, Any]] = None,
|
||||
moderator_id_override: Optional[int] = None
|
||||
) -> Embed:
|
||||
"""Helper function to create the standard log embed."""
|
||||
moderator_id_override: Optional[int] = None,
|
||||
) -> ui.LayoutView:
|
||||
"""Helper function to create the standard log view."""
|
||||
color_map = {
|
||||
"BAN": Color.red(),
|
||||
"UNBAN": Color.green(),
|
||||
@ -265,87 +281,56 @@ class ModLogCog(commands.Cog):
|
||||
"AI_ALERT": Color.purple(),
|
||||
"AI_DELETE_REQUESTED": Color.dark_grey(),
|
||||
}
|
||||
# Use a distinct color for AI actions
|
||||
if source == "AI_API":
|
||||
embed_color = Color.blurple()
|
||||
else:
|
||||
embed_color = color_map.get(action_type.upper(), Color.greyple())
|
||||
embed_color = Color.blurple() if source == "AI_API" else color_map.get(action_type.upper(), Color.greyple())
|
||||
action_title_prefix = "🤖 AI Moderation Action" if source == "AI_API" else action_type.replace("_", " ").title()
|
||||
action_title = f"{action_title_prefix} | Case #{case_id}"
|
||||
|
||||
embed = Embed(
|
||||
title=action_title,
|
||||
color=embed_color,
|
||||
timestamp=discord.utils.utcnow()
|
||||
)
|
||||
|
||||
target_display = self._format_user(target, guild)
|
||||
|
||||
if source == "AI_API":
|
||||
moderator_display = f"AI System (ID: {moderator_id_override or 'Unknown'})"
|
||||
else:
|
||||
moderator_display = self._format_user(moderator, guild)
|
||||
|
||||
|
||||
embed.add_field(name="User", value=target_display, inline=True)
|
||||
embed.add_field(name="Moderator", value=moderator_display, inline=True)
|
||||
|
||||
# Add AI-specific details if available
|
||||
moderator_display = (
|
||||
f"AI System (ID: {moderator_id_override or 'Unknown'})" if source == "AI_API" else self._format_user(moderator, guild)
|
||||
)
|
||||
lines = [f"**User:** {target_display}", f"**Moderator:** {moderator_display}"]
|
||||
if ai_details:
|
||||
if 'rule_violated' in ai_details:
|
||||
embed.add_field(name="Rule Violated", value=ai_details['rule_violated'], inline=True)
|
||||
if 'reasoning' in ai_details:
|
||||
# Use AI reasoning as the main reason field if bot reason is empty
|
||||
reason_to_display = reason or ai_details['reasoning']
|
||||
embed.add_field(name="Reason / AI Reasoning", value=reason_to_display or "No reason provided.", inline=False)
|
||||
# Optionally add bot reason separately if both exist and differ
|
||||
if reason and reason != ai_details['reasoning']:
|
||||
embed.add_field(name="Original Bot Reason", value=reason, inline=False)
|
||||
if "rule_violated" in ai_details:
|
||||
lines.append(f"**Rule Violated:** {ai_details['rule_violated']}")
|
||||
if "reasoning" in ai_details:
|
||||
reason_to_display = reason or ai_details["reasoning"]
|
||||
lines.append(f"**Reason / AI Reasoning:** {reason_to_display or 'No reason provided.'}")
|
||||
if reason and reason != ai_details["reasoning"]:
|
||||
lines.append(f"**Original Bot Reason:** {reason}")
|
||||
else:
|
||||
embed.add_field(name="Reason", value=reason or "No reason provided.", inline=False)
|
||||
|
||||
# Add full message content if available
|
||||
if 'message_content' in ai_details:
|
||||
# Truncate if too long (Discord has a 1024 character limit for embed fields)
|
||||
message_content = ai_details['message_content']
|
||||
lines.append(f"**Reason:** {reason or 'No reason provided.'}")
|
||||
if "message_content" in ai_details:
|
||||
message_content = ai_details["message_content"]
|
||||
if len(message_content) > 1000:
|
||||
message_content = message_content[:997] + "..."
|
||||
embed.add_field(name="Message Content", value=message_content, inline=False)
|
||||
lines.append(f"**Message Content:** {message_content}")
|
||||
else:
|
||||
embed.add_field(name="Reason", value=reason or "No reason provided.", inline=False)
|
||||
|
||||
lines.append(f"**Reason:** {reason or 'No reason provided.'}")
|
||||
if duration:
|
||||
# Format duration nicely (e.g., "1 day", "2 hours 30 minutes")
|
||||
# This is a simple version, could be made more robust
|
||||
total_seconds = int(duration.total_seconds())
|
||||
days, remainder = divmod(total_seconds, 86400)
|
||||
hours, remainder = divmod(remainder, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
duration_str = ""
|
||||
if days > 0: duration_str += f"{days}d "
|
||||
if hours > 0: duration_str += f"{hours}h "
|
||||
if minutes > 0: duration_str += f"{minutes}m "
|
||||
if seconds > 0 or not duration_str: duration_str += f"{seconds}s"
|
||||
if days > 0:
|
||||
duration_str += f"{days}d "
|
||||
if hours > 0:
|
||||
duration_str += f"{hours}h "
|
||||
if minutes > 0:
|
||||
duration_str += f"{minutes}m "
|
||||
if seconds > 0 or not duration_str:
|
||||
duration_str += f"{seconds}s"
|
||||
duration_str = duration_str.strip()
|
||||
|
||||
embed.add_field(name="Duration", value=duration_str, inline=True)
|
||||
# Add expiration timestamp if applicable (e.g., for timeouts)
|
||||
lines.append(f"**Duration:** {duration_str}")
|
||||
if action_type.upper() == "TIMEOUT":
|
||||
expires_at = discord.utils.utcnow() + duration
|
||||
embed.add_field(name="Expires", value=f"<t:{int(expires_at.timestamp())}:R>", inline=True)
|
||||
|
||||
|
||||
if source == "AI_API":
|
||||
ai_model = ai_details.get("ai_model") if ai_details else None
|
||||
embed.set_footer(
|
||||
text=f"AI Moderation Action • {guild.name} ({guild.id})" + (f" • Model: {ai_model}" if ai_model else ""),
|
||||
icon_url="https://cdn-icons-png.flaticon.com/512/4712/4712035.png"
|
||||
)
|
||||
else:
|
||||
embed.set_footer(text=f"Guild: {guild.name} ({guild.id})")
|
||||
|
||||
return embed
|
||||
|
||||
expires_at = discord.utils.utcnow() + duration
|
||||
lines.append(f"**Expires:** <t:{int(expires_at.timestamp())}:R>")
|
||||
footer = (
|
||||
f"AI Moderation Action • {guild.name} ({guild.id})" + (f" • Model: {ai_details.get('ai_model')}" if ai_details and ai_details.get('ai_model') else "")
|
||||
if source == "AI_API"
|
||||
else f"Guild: {guild.name} ({guild.id})"
|
||||
)
|
||||
return self.LogView(self.bot, action_title, embed_color, lines, footer)
|
||||
# --- View Command Callback ---
|
||||
@app_commands.checks.has_permissions(moderate_members=True) # Adjust permissions as needed
|
||||
async def modlog_view_callback(self, interaction: Interaction, user: Optional[discord.User] = None):
|
||||
@ -430,7 +415,7 @@ class ModLogCog(commands.Cog):
|
||||
|
||||
duration = datetime.timedelta(seconds=record['duration_seconds']) if record['duration_seconds'] else None
|
||||
|
||||
embed = self._format_log_embed(
|
||||
view = self._format_log_embed(
|
||||
case_id,
|
||||
moderator or Object(id=record['moderator_id']), # Fallback to Object if user not found
|
||||
target or Object(id=record['target_user_id']), # Fallback to Object if user not found
|
||||
@ -443,9 +428,10 @@ class ModLogCog(commands.Cog):
|
||||
# Add log message link if available
|
||||
if record['log_message_id'] and record['log_channel_id']:
|
||||
link = f"https://discord.com/channels/{record['guild_id']}/{record['log_channel_id']}/{record['log_message_id']}"
|
||||
embed.add_field(name="Log Message", value=f"[Jump to Log]({link})", inline=False)
|
||||
# Append jump link as extra line
|
||||
view.footer_display.content += f" | [Jump to Log]({link})"
|
||||
|
||||
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||
await interaction.followup.send(view=view, ephemeral=True)
|
||||
|
||||
|
||||
@app_commands.checks.has_permissions(manage_guild=True) # Higher permission for editing reasons
|
||||
@ -477,7 +463,7 @@ class ModLogCog(commands.Cog):
|
||||
log_channel = interaction.guild.get_channel(original_record['log_channel_id'])
|
||||
if log_channel and isinstance(log_channel, discord.TextChannel):
|
||||
log_message = await log_channel.fetch_message(original_record['log_message_id'])
|
||||
if log_message and log_message.author == self.bot.user and log_message.embeds:
|
||||
if log_message and log_message.author == self.bot.user:
|
||||
# Re-fetch users/duration to reconstruct embed accurately
|
||||
# Special handling for AI moderator (ID 0) to avoid Discord API 404 error
|
||||
if original_record['moderator_id'] == 0:
|
||||
@ -498,7 +484,7 @@ class ModLogCog(commands.Cog):
|
||||
|
||||
duration = datetime.timedelta(seconds=original_record['duration_seconds']) if original_record['duration_seconds'] else None
|
||||
|
||||
new_embed = self._format_log_embed(
|
||||
new_view = self._format_log_embed(
|
||||
case_id,
|
||||
moderator or Object(id=original_record['moderator_id']),
|
||||
target or Object(id=original_record['target_user_id']),
|
||||
@ -507,13 +493,11 @@ class ModLogCog(commands.Cog):
|
||||
duration,
|
||||
interaction.guild
|
||||
)
|
||||
# Add log message link again
|
||||
link = f"https://discord.com/channels/{original_record['guild_id']}/{original_record['log_channel_id']}/{original_record['log_message_id']}"
|
||||
new_embed.add_field(name="Log Message", value=f"[Jump to Log]({link})", inline=False)
|
||||
new_embed.add_field(name="Updated Reason By", value=f"{interaction.user.mention}", inline=False) # Indicate update
|
||||
new_view.footer_display.content += f" | [Jump to Log]({link}) | Updated By: {interaction.user.mention}"
|
||||
|
||||
await log_message.edit(embed=new_embed)
|
||||
log.info(f"Successfully updated log message embed for case {case_id}")
|
||||
await log_message.edit(view=new_view)
|
||||
log.info(f"Successfully updated log message view for case {case_id}")
|
||||
except discord.NotFound:
|
||||
log.warning(f"Original log message or channel not found for case {case_id} when updating reason.")
|
||||
except discord.Forbidden:
|
||||
|
Loading…
x
Reference in New Issue
Block a user