# ╔══════════════════════════════════════════════════════════════════╗ # ║ ║ # ║ +-+-+-+-+-+-+-+-+ ║ # ║ |H|e|x|a|H|o|s|t| ║ # ║ +-+-+-+-+-+-+-+-+ ║ # ║ ║ # ║ © 2026 HexaHost — All Rights Reserved ║ # ║ ║ # ║ discord ── https://discord.gg/hexahost ║ # ║ github ── https://github.com/theoneandonlymace ║ # ║ ║ # ╚══════════════════════════════════════════════════════════════════╝ from utils.db_paths import jsondb_path import discord from discord.ext import commands from discord.ui import View, Select, Button import logging import time import asyncio import aiofiles import json from typing import List, Optional, Dict, Any, Union from datetime import datetime, timedelta from pathlib import Path import os import re logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) LOG_CATEGORIES = [ "message_events", "join_leave_events", "member_moderation", "voice_events", "channel_events", "role_events", "emoji_events", "reaction_events", "system_events", ] CONFIG_FILE = jsondb_path("logging_config.json") LOGS_DIR = "logs" MAX_AUDIT_CACHE_SIZE = 1000 AUDIT_CACHE_TTL = 300 MAX_LOG_FILE_SIZE = 10 * 1024 * 1024 MAX_SEARCH_RESULTS = 100 Path(LOGS_DIR).mkdir(exist_ok=True) from utils.cv2 import CV2, build_container from discord.ui import TextDisplay, Separator, ActionRow, LayoutView, Container, Select class LogSetupLayoutView(LayoutView): def __init__(self, bot, author, categories): super().__init__(timeout=300) self.bot = bot self.author = author self.categories = categories self.selected_channels = {cat: None for cat in LOG_CATEGORIES} self.select = Select( placeholder="Choose a logging category to configure...", options=[ discord.SelectOption( label=cat.replace("_", " ").title(), value=cat, description=f"Configure {cat.replace('_', ' ').lower()} logging", ) for cat in LOG_CATEGORIES ], ) self.select.callback = self.category_select_callback self.add_item( Container( TextDisplay("**Logging Setup**"), Separator(visible=True), TextDisplay( f"Configure logging channels for different event categories.\n\nGuild ID: {author.guild.id}" ), ActionRow(self.select), ) ) async def category_select_callback(self, interaction: discord.Interaction): if interaction.user != self.author: await interaction.response.send_message( "Only the command author can interact.", ephemeral=True ) return category = interaction.data.get("values", [None])[0] if not category: return cog = self.bot.get_cog("Logging") if not cog: await interaction.response.send_message( "Logging cog not found.", ephemeral=True ) return channel_view = ChannelSelectView(self.bot, self.author, category, None, None) layout_view = CV2( f"Select Channel for {category.replace('_', ' ').title()}", f"Choose where to log {category.replace('_', ' ').lower()} events.\n\nPage 1 of {channel_view.total_pages}", ) for container in layout_view.children: if type(container).__name__ == "Container": if channel_view.children: add_action_rows(container, channel_view.children) break await interaction.response.edit_message(view=layout_view) @discord.ui.button(label="Finish Setup", style=discord.ButtonStyle.success) async def finish_button(self, interaction: discord.Interaction, button: Button): await self.finish_setup(interaction) async def finish_setup(self, interaction: discord.Interaction): try: log_enabled = { cat: bool(self.selected_channels.get(cat)) for cat in LOG_CATEGORIES } cog = self.bot.get_cog("Logging") if not cog: await interaction.response.send_message( "Logging cog not found.", ephemeral=True ) return await cog._save_log_config( interaction.guild.id, self.selected_channels, log_enabled, [], [], [], None, ) configured_count = sum( 1 for ch in self.selected_channels.values() if ch is not None ) layout_view = CV2( "Setup Complete", f"Logging has been configured successfully.\n\n**Configuration Summary**\nConfigured {configured_count} out of {len(LOG_CATEGORIES)} categories", ) for item in self.children: if hasattr(item, "disabled"): item.disabled = True await interaction.response.edit_message(view=layout_view) except Exception as e: logger.error(f"Error finishing setup: {e}") pass class CV2EmbedAdapter(CV2): def __init__(self, t, d="", **kwargs): self._title = t self._description = d or "" self._fields = [] self._footer = None super().__init__(t, self._description) self.color = kwargs.get("color", 0xFF0000) def _rebuild(self): self.clear_items() sections = [self._description] if self._description else [] for name, value in self._fields: sections.append(f"**{name}**\n{value}") if self._footer: sections.append(f"*{self._footer}*") container = build_container( TextDisplay(f"**{self._title}**"), *[ item for s in sections for item in (Separator(visible=True), TextDisplay(str(s))) ], ) self.add_item(container) def add_field(self, name, value, inline=False, **kwargs): self._fields.append((name, value)) self._rebuild() return self def set_footer(self, text=None, icon_url=None, **kwargs): if text: self._footer = text self._rebuild() return self def set_thumbnail(self, url=None, **kwargs): return self def set_author(self, name=None, url=None, icon_url=None, **kwargs): return self def set_image(self, url=None, **kwargs): return self def to_dict(self): return {"title": self._title, "description": self._description} def add_action_rows(container, components): from discord.ui import ActionRow current_row = [] for item in components: # Check if it's a select menu or any other full-width component. Select Menu is type 3. # ChannelSelect is 8, RoleSelect is 6, UserSelect is 5, MentionableSelect is 7, Text Input is 4. # Generally, anything other than Button (type 2) takes a full row. if getattr(item, "type", None) and item.type.value != 2: if current_row: container.add_item(ActionRow(*current_row)) current_row = [] container.add_item(ActionRow(item)) else: current_row.append(item) if len(current_row) == 5: container.add_item(ActionRow(*current_row)) current_row = [] if current_row: container.add_item(ActionRow(*current_row)) class LogSetupView(View): """Enhanced view for configuring logging settings with improved UI.""" def __init__(self, bot: commands.Bot, author: discord.Member, timeout: float = 300): super().__init__(timeout=timeout) self.bot = bot self.author = author self.selected_channels: Dict[str, Optional[int]] = { cat: None for cat in LOG_CATEGORIES } self.message: Optional[discord.Message] = None self.current_step = 0 async def interaction_check(self, interaction: discord.Interaction) -> bool: """Ensure only the command author can interact.""" if interaction.user != self.author: try: if not interaction.response.is_done(): await interaction.response.send_message( "Only the command author can interact with this menu.", ephemeral=True, ) except (discord.NotFound, discord.HTTPException): pass return False return True async def on_timeout(self): """Handle view timeout with proper cleanup.""" if self.message: try: for item in self.children: item.disabled = True await self.message.edit(view=self) logger.info( f"Log setup timed out for {self.author} in {self.author.guild.name}" ) except (discord.NotFound, discord.HTTPException): try: await self.message.delete() except: pass @discord.ui.select(placeholder="Choose a logging category to configure...") async def category_select(self, interaction: discord.Interaction, select: Select): """Handle category selection in setup.""" try: if select.values[0] == "finish": await self.finish_setup(interaction) return category = select.values[0] guild = interaction.guild channel_view = ChannelSelectView(self.bot, self.author, category, self) from utils.cv2 import CV2 from discord.ui import ActionRow layout_view = CV2( f"Select Channel for {category.replace('_', ' ').title()}", f"Choose where to log {category.replace('_', ' ').lower()} events.\n\n**Page 1 of {channel_view.total_pages}** - Showing {len(channel_view.all_channels[: channel_view.channels_per_page])} channels", ) for container in layout_view.children: if type(container).__name__ == "Container": if channel_view.children: add_action_rows(container, channel_view.children) break await interaction.response.edit_message(embed=None, view=layout_view) except Exception as e: logger.error(f"Error in category selection: {e}") pass @discord.ui.button(label="Finish Setup", style=discord.ButtonStyle.success) async def finish_button(self, interaction: discord.Interaction, button: Button): """Finish the setup process.""" await self.finish_setup(interaction) async def finish_setup(self, interaction: discord.Interaction): """Complete the setup process.""" try: log_enabled = { cat: bool(self.selected_channels.get(cat)) for cat in LOG_CATEGORIES } cog = self.bot.get_cog("Logging") if not cog: await interaction.response.send_message( "Logging cog not found.", ephemeral=True ) return await cog._save_log_config( interaction.guild.id, self.selected_channels, log_enabled, [], [], [], None, ) from utils.cv2 import CV2 from discord.ui import ActionRow configured_count = sum( 1 for ch in self.selected_channels.values() if ch is not None ) layout_view = CV2( "Setup Complete", "Logging has been configured successfully.", f"**Configuration Summary**\nConfigured {configured_count} out of {len(LOG_CATEGORIES)} categories\n\n*Use /log status to view your configuration*", ) for item in self.children: item.disabled = True for container in layout_view.children: if type(container).__name__ == "Container": if self.children: add_action_rows(container, self.children) break await interaction.response.edit_message(embed=None, view=layout_view) except Exception as e: logger.error(f"Error finishing setup: {e}") try: pass except: pass class InteractiveConfigView(View): """Fully interactive configuration view for all logging settings.""" def __init__(self, bot: commands.Bot, author: discord.Member, config: Dict): super().__init__(timeout=300) self.bot = bot self.author = author self.config = config self.message: Optional[discord.Message] = None async def interaction_check(self, interaction: discord.Interaction) -> bool: """Ensure only the command author can interact.""" if interaction.user != self.author: try: if not interaction.response.is_done(): await interaction.response.send_message( "Only the command author can interact with this menu.", ephemeral=True, ) except (discord.NotFound, discord.HTTPException): pass return False return True @discord.ui.button(label="Change Channels", style=discord.ButtonStyle.primary) async def change_channels(self, interaction: discord.Interaction, button: Button): """Open channel configuration menu.""" view = ChannelConfigView(self.bot, self.author, self.config) from utils.cv2 import CV2 from discord.ui import ActionRow layout_view = CV2( "Channel Configuration", "Select a category to change its log channel.", f"Guild ID: {interaction.guild.id}", ) for container in layout_view.children: if type(container).__name__ == "Container": if view.children: add_action_rows(container, view.children) break await interaction.response.edit_message(embed=None, view=layout_view) @discord.ui.button(label="Toggle Categories", style=discord.ButtonStyle.secondary) async def toggle_categories(self, interaction: discord.Interaction, button: Button): """Open category toggle menu.""" view = CategoryToggleView(self.bot, self.author, self.config) from utils.cv2 import CV2 from discord.ui import ActionRow layout_view = CV2( "Toggle Categories", "Enable or disable logging categories.", f"Guild ID: {interaction.guild.id}", ) for container in layout_view.children: if type(container).__name__ == "Container": if view.children: add_action_rows(container, view.children) break await interaction.response.edit_message(embed=None, view=layout_view) @discord.ui.button(label="Manage Ignores", style=discord.ButtonStyle.secondary) async def manage_ignores(self, interaction: discord.Interaction, button: Button): """Open ignore management menu.""" view = IgnoreManagementView(self.bot, self.author, self.config) from utils.cv2 import CV2 from discord.ui import ActionRow layout_view = CV2( "Ignore Management", "Manage ignored channels, roles, and users.", f"Guild ID: {interaction.guild.id}", ) for container in layout_view.children: if type(container).__name__ == "Container": if view.children: add_action_rows(container, view.children) break await interaction.response.edit_message(embed=None, view=layout_view) @discord.ui.button( label="Auto-Delete Settings", style=discord.ButtonStyle.secondary ) async def auto_delete_settings( self, interaction: discord.Interaction, button: Button ): """Configure auto-delete duration.""" view = AutoDeleteView(self.bot, self.author, self.config) current_duration = self.config.get("auto_delete_duration") current_text = { None: "Disabled", 3600: "1 Hour", 86400: "24 Hours", 604800: "7 Days", }.get(current_duration, f"{current_duration} seconds") from utils.cv2 import CV2 from discord.ui import ActionRow layout_view = CV2( "Auto-Delete Settings", f"Current Setting: {current_text}", f"Guild ID: {interaction.guild.id}", ) for container in layout_view.children: if type(container).__name__ == "Container": if view.children: add_action_rows(container, view.children) break await interaction.response.edit_message(embed=None, view=layout_view) class ChannelConfigView(View): """View for configuring log channels.""" def __init__(self, bot: commands.Bot, author: discord.Member, config: Dict): super().__init__(timeout=300) self.bot = bot self.author = author self.config = config self.add_item(self.create_category_select()) def create_category_select(self) -> Select: """Create select menu for category selection.""" options = [] log_channels = self.config.get("log_channels", {}) for category in LOG_CATEGORIES: channel_id = log_channels.get(category) channel = self.author.guild.get_channel(channel_id) if channel_id else None description = f"Currently: {channel.name}" if channel else "Not configured" options.append( discord.SelectOption( label=category.replace("_", " ").title(), value=category, description=description[:100], ) ) select = Select(placeholder="Select category to configure...", options=options) select.callback = self.category_select_callback return select async def category_select_callback(self, interaction: discord.Interaction): """Handle category selection.""" category = interaction.data["values"][0] view = ChannelSelectView(self.bot, self.author, category, None, self.config) from utils.cv2 import CV2 from discord.ui import ActionRow layout_view = CV2( f"Select Channel for {category.replace('_', ' ').title()}", f"Choose where to log {category.replace('_', ' ').lower()} events.\n\n**Page 1 of {view.total_pages}** - Showing {len(view.all_channels[: view.channels_per_page])} channels", f"Guild ID: {interaction.guild.id}", ) for container in layout_view.children: if type(container).__name__ == "Container": if view.children: add_action_rows(container, view.children) break await interaction.response.edit_message(embed=None, view=layout_view) class CategoryToggleView(View): """View for toggling categories.""" def __init__(self, bot: commands.Bot, author: discord.Member, config: Dict): super().__init__(timeout=300) self.bot = bot self.author = author self.config = config self.add_item(self.create_toggle_select()) def create_toggle_select(self) -> Select: """Create select menu for toggling categories.""" options = [] log_enabled = self.config.get("log_enabled", {}) for category in LOG_CATEGORIES: enabled = log_enabled.get(category, False) status = "Enabled" if enabled else "Disabled" options.append( discord.SelectOption( label=category.replace("_", " ").title(), value=category, description=f"Currently: {status}", ) ) select = Select(placeholder="Select category to toggle...", options=options) select.callback = self.toggle_callback return select async def toggle_callback(self, interaction: discord.Interaction): """Handle category toggle.""" category = interaction.data["values"][0] log_enabled = self.config.get("log_enabled", {}) current_state = log_enabled.get(category, False) new_state = not current_state log_enabled[category] = new_state cog = self.bot.get_cog("Logging") await cog._save_log_config( interaction.guild.id, self.config.get("log_channels", {}), log_enabled, self.config.get("ignore_channels", []), self.config.get("ignore_roles", []), self.config.get("ignore_users", []), self.config.get("auto_delete_duration"), ) self.config["log_enabled"] = log_enabled status = "enabled" if new_state else "disabled" from utils.cv2 import CV2 from discord.ui import ActionRow self.clear_items() self.add_item(self.create_toggle_select()) layout_view = CV2( "Category Updated", f"{category.replace('_', ' ').title()} logging has been {status}.", f"Guild ID: {interaction.guild.id}", ) for container in layout_view.children: if type(container).__name__ == "Container": if self.children: add_action_rows(container, self.children) break await interaction.response.edit_message(embed=None, view=layout_view) class IgnoreManagementView(View): """View for managing ignore lists.""" def __init__(self, bot: commands.Bot, author: discord.Member, config: Dict): super().__init__(timeout=300) self.bot = bot self.author = author self.config = config @discord.ui.button(label="View Ignored", style=discord.ButtonStyle.secondary) async def view_ignored(self, interaction: discord.Interaction, button: Button): """Show current ignore lists.""" ignore_channels = self.config.get("ignore_channels", []) ignore_roles = self.config.get("ignore_roles", []) ignore_users = self.config.get("ignore_users", []) channels_text = ( "\n".join( [ f"<#{cid}>" for cid in ignore_channels if interaction.guild.get_channel(cid) ] ) or "None" ) roles_text = ( "\n".join( [ f"<@&{rid}>" for rid in ignore_roles if interaction.guild.get_role(rid) ] ) or "None" ) users_text = ( "\n".join( [ f"<@{uid}>" for uid in ignore_users if interaction.guild.get_member(uid) ] ) or "None" ) from utils.cv2 import CV2 from discord.ui import ActionRow layout_view = CV2( "Current Ignore Lists", f"**Ignored Channels**\n{channels_text[:1024]}", f"**Ignored Roles**\n{roles_text[:1024]}", f"**Ignored Users**\n{users_text[:1024]}", f"Guild ID: {interaction.guild.id}", ) for container in layout_view.children: if type(container).__name__ == "Container": if self.children: add_action_rows(container, self.children) break await interaction.response.edit_message(embed=None, view=layout_view) @discord.ui.button(label="Manage Channels", style=discord.ButtonStyle.primary) async def manage_channels(self, interaction: discord.Interaction, button: Button): """Manage ignored channels.""" from utils.cv2 import CV2 from discord.ui import ActionRow layout_view = CV2( "Manage Ignored Channels", "Use /log ignore add channel #channel or /log ignore remove channel #channel", f"Guild ID: {interaction.guild.id}", ) for container in layout_view.children: if type(container).__name__ == "Container": if self.children: add_action_rows(container, self.children) break await interaction.response.edit_message(embed=None, view=layout_view) @discord.ui.button(label="Manage Roles", style=discord.ButtonStyle.primary) async def manage_roles(self, interaction: discord.Interaction, button: Button): """Manage ignored roles.""" from utils.cv2 import CV2 from discord.ui import ActionRow layout_view = CV2( "Manage Ignored Roles", "Use /log ignore add role @role or /log ignore remove role @role", f"Guild ID: {interaction.guild.id}", ) for container in layout_view.children: if type(container).__name__ == "Container": if self.children: add_action_rows(container, self.children) break await interaction.response.edit_message(embed=None, view=layout_view) @discord.ui.button(label="Manage Users", style=discord.ButtonStyle.primary) async def manage_users(self, interaction: discord.Interaction, button: Button): """Manage ignored users.""" from utils.cv2 import CV2 from discord.ui import ActionRow layout_view = CV2( "Manage Ignored Users", "Use /log ignore add user @user or /log ignore remove user @user", f"Guild ID: {interaction.guild.id}", ) for container in layout_view.children: if type(container).__name__ == "Container": if self.children: add_action_rows(container, self.children) break await interaction.response.edit_message(embed=None, view=layout_view) class AutoDeleteView(View): """View for configuring auto-delete duration.""" def __init__(self, bot: commands.Bot, author: discord.Member, config: Dict): super().__init__(timeout=300) self.bot = bot self.author = author self.config = config self.add_item(self.create_duration_select()) def create_duration_select(self) -> Select: """Create select menu for auto-delete duration.""" options = [ discord.SelectOption( label="Disabled", value="None", description="Never auto-delete logs" ), discord.SelectOption( label="1 Hour", value="3600", description="Delete logs after 1 hour" ), discord.SelectOption( label="24 Hours", value="86400", description="Delete logs after 24 hours", ), discord.SelectOption( label="7 Days", value="604800", description="Delete logs after 7 days" ), ] select = Select(placeholder="Select auto-delete duration...", options=options) select.callback = self.duration_callback return select async def duration_callback(self, interaction: discord.Interaction): """Handle duration selection.""" duration_str = interaction.data["values"][0] duration = None if duration_str == "None" else int(duration_str) cog = self.bot.get_cog("Logging") await cog._save_log_config( interaction.guild.id, self.config.get("log_channels", {}), self.config.get("log_enabled", {}), self.config.get("ignore_channels", []), self.config.get("ignore_roles", []), self.config.get("ignore_users", []), duration, ) self.config["auto_delete_duration"] = duration duration_text = { None: "Disabled", 3600: "1 Hour", 86400: "24 Hours", 604800: "7 Days", }.get(duration, f"{duration} seconds") from utils.cv2 import CV2 from discord.ui import ActionRow layout_view = CV2( "Auto-Delete Updated", f"Auto-delete duration set to: {duration_text}", f"Guild ID: {interaction.guild.id}", ) for container in layout_view.children: if type(container).__name__ == "Container": if self.children: add_action_rows(container, self.children) break await interaction.response.edit_message(embed=None, view=layout_view) class ChannelSelectView(View): """View for selecting channels for specific categories with pagination.""" def __init__( self, bot: commands.Bot, author: discord.Member, category: str, parent_view: LogSetupView, config: Dict = None, ): super().__init__(timeout=300) self.bot = bot self.author = author self.category = category self.parent_view = parent_view self.config = config self.current_page = 0 self.channels_per_page = 24 self.all_channels = [ ch for ch in author.guild.text_channels if ch.permissions_for(author.guild.me).send_messages ] self.total_pages = max( 1, (len(self.all_channels) + self.channels_per_page - 1) // self.channels_per_page, ) self.add_item(self.create_channel_select()) if self.total_pages > 1: self.add_navigation_buttons() async def interaction_check(self, interaction: discord.Interaction) -> bool: """Ensure only the command author can interact.""" if interaction.user != self.author: try: if not interaction.response.is_done(): await interaction.response.send_message( "Only the command author can interact with this menu.", ephemeral=True, ) except (discord.NotFound, discord.HTTPException): pass return False return True def create_channel_select(self) -> Select: """Create select menu for channel selection with pagination.""" start_idx = self.current_page * self.channels_per_page end_idx = start_idx + self.channels_per_page current_channels = self.all_channels[start_idx:end_idx] options = [ discord.SelectOption( label=channel.name[:100], value=str(channel.id), description=f"#{channel.name} in {channel.category.name if channel.category else 'No Category'}"[ :100 ], ) for channel in current_channels ] options.append( discord.SelectOption( label="Skip this category", value="skip", description="Don't configure this category", ) ) select = Select( placeholder=f"Select channel for {self.category.replace('_', ' ').title()} (Page {self.current_page + 1}/{self.total_pages})", options=options, custom_id="channel_select", ) select.callback = self.channel_select_callback return select def add_navigation_buttons(self): """Add navigation buttons for pagination.""" if self.total_pages > 1: prev_button = Button( label="◀ Previous", style=discord.ButtonStyle.secondary, disabled=self.current_page == 0, custom_id="prev_page", ) prev_button.callback = self.previous_page self.add_item(prev_button) next_button = Button( label="Next ▶", style=discord.ButtonStyle.secondary, disabled=self.current_page >= self.total_pages - 1, custom_id="next_page", ) next_button.callback = self.next_page self.add_item(next_button) async def previous_page(self, interaction: discord.Interaction): """Go to previous page.""" if self.current_page > 0: self.current_page -= 1 await self.update_view(interaction) async def next_page(self, interaction: discord.Interaction): """Go to next page.""" if self.current_page < self.total_pages - 1: self.current_page += 1 await self.update_view(interaction) async def update_view(self, interaction: discord.Interaction): """Update the view with new page content.""" self.clear_items() self.add_item(self.create_channel_select()) if self.total_pages > 1: self.add_navigation_buttons() from utils.cv2 import CV2 from discord.ui import ActionRow layout_view = CV2( f"Select Channel for {self.category.replace('_', ' ').title()}", f"Choose where to log {self.category.replace('_', ' ').lower()} events.\n\n**Page {self.current_page + 1} of {self.total_pages}** - Showing {len(self.all_channels[self.current_page * self.channels_per_page : (self.current_page + 1) * self.channels_per_page])} channels", f"Guild ID: {interaction.guild.id}", ) for container in layout_view.children: if type(container).__name__ == "Container": if self.children: add_action_rows(container, self.children) break await interaction.response.edit_message(embed=None, view=layout_view) async def channel_select_callback(self, interaction: discord.Interaction): """Handle channel selection.""" try: select = [item for item in self.children if isinstance(item, Select)][0] if self.config: if select.values[0] == "skip": channel_id = None else: channel_id = int(select.values[0]) log_channels = self.config.get("log_channels", {}) if channel_id: log_channels[self.category] = channel_id elif self.category in log_channels: del log_channels[self.category] cog = self.bot.get_cog("Logging") await cog._save_log_config( interaction.guild.id, log_channels, self.config.get("log_enabled", {}), self.config.get("ignore_channels", []), self.config.get("ignore_roles", []), self.config.get("ignore_users", []), self.config.get("auto_delete_duration"), ) self.config["log_channels"] = log_channels channel = ( interaction.guild.get_channel(channel_id) if channel_id else None ) channel_text = channel.mention if channel else "None" config_view = InteractiveConfigView(self.bot, self.author, self.config) from utils.cv2 import CV2 from discord.ui import ActionRow layout_view = CV2( "Channel Updated", f"{self.category.replace('_', ' ').title()} logs will be sent to: {channel_text}", f"Guild ID: {interaction.guild.id}", ) for container in layout_view.children: if type(container).__name__ == "Container": if config_view.children: add_action_rows(container, config_view.children) break await interaction.response.edit_message(embed=None, view=layout_view) else: if self.parent_view is None: cog = self.bot.get_cog("Logging") if not cog: await interaction.response.send_message( "Logging cog not found.", ephemeral=True ) return channel_id = ( None if select.values[0] == "skip" else int(select.values[0]) ) log_channels = {self.category: channel_id} log_enabled = {cat: bool(channel_id) for cat in LOG_CATEGORIES} await cog._save_log_config( interaction.guild.id, log_channels, log_enabled, [], [], [], None, ) channel = ( interaction.guild.get_channel(channel_id) if channel_id else None ) channel_text = channel.mention if channel else "None" from utils.cv2 import CV2 layout_view = CV2( "Channel Updated", f"{self.category.replace('_', ' ').title()} logs will be sent to: {channel_text}", f"Guild ID: {interaction.guild.id}", ) await interaction.response.edit_message(view=layout_view) else: if select.values[0] == "skip": self.parent_view.selected_channels[self.category] = None else: self.parent_view.selected_channels[self.category] = int( select.values[0] ) await self.return_to_setup(interaction) except Exception as e: logger.error(f"Error in channel selection callback: {e}") pass @discord.ui.button(label="Back to Setup", style=discord.ButtonStyle.secondary) async def back_button(self, interaction: discord.Interaction, button: Button): """Return to main setup view.""" if self.config: config_view = InteractiveConfigView(self.bot, self.author, self.config) from utils.cv2 import CV2 from discord.ui import ActionRow layout_view = CV2( "Logging Configuration", "Use the buttons below to modify your logging settings.", f"Guild ID: {interaction.guild.id}", ) for container in layout_view.children: if type(container).__name__ == "Container": if config_view.children: add_action_rows(container, config_view.children) break await interaction.response.edit_message(embed=None, view=layout_view) else: await self.return_to_setup(interaction) async def return_to_setup(self, interaction: discord.Interaction): """Return to the main setup view.""" try: embed = CV2EmbedAdapter( "Logging Setup", "Configure logging channels for different event categories.", ) for category in LOG_CATEGORIES: channel_id = self.parent_view.selected_channels.get(category) if channel_id: channel = interaction.guild.get_channel(channel_id) status = channel.mention if channel else "Configured" else: status = "Not configured" embed.add_field( name=category.replace("_", " ").title(), value=status, inline=True ) embed.set_footer(text=f"Guild ID: {interaction.guild.id}") options = [] for category in LOG_CATEGORIES: configured = bool(self.parent_view.selected_channels.get(category)) options.append( discord.SelectOption( label=category.replace("_", " ").title(), value=category, description=f"Configure {category.replace('_', ' ').lower()} logging", ) ) options.append( discord.SelectOption( label="Finish Setup", value="finish", description="Complete the setup process", ) ) self.parent_view.category_select.options = options from discord.ui import ActionRow add_action_rows(embed, self.parent_view.children) await interaction.response.edit_message(embed=None, view=embed) except Exception as e: logger.error(f"Error returning to setup: {e}") pass class Logging(commands.Cog): """Comprehensive logging cog with modern UI and expanded event coverage.""" def __init__(self, bot: commands.Bot): self.bot = bot self.config_cache: Dict[int, Dict[str, Any]] = {} self.audit_cache: Dict[tuple, tuple[Any, float]] = {} self.config_lock = asyncio.Lock() self.log_queue: Dict[int, List[Dict]] = {} asyncio.create_task(self._load_config()) asyncio.create_task(self._periodic_cache_cleanup()) async def _periodic_cache_cleanup(self): """Periodically clean up audit cache to prevent memory leaks.""" while True: try: await asyncio.sleep(AUDIT_CACHE_TTL) current_time = time.time() expired_keys = [ key for key, (_, timestamp) in self.audit_cache.items() if current_time - timestamp > AUDIT_CACHE_TTL ] for key in expired_keys: del self.audit_cache[key] if len(self.audit_cache) > MAX_AUDIT_CACHE_SIZE: sorted_items = sorted( self.audit_cache.items(), key=lambda x: x[1][1] ) for key, _ in sorted_items[ : len(self.audit_cache) - MAX_AUDIT_CACHE_SIZE ]: del self.audit_cache[key] logger.debug( f"Cache cleanup: {len(self.audit_cache)} entries remaining" ) except Exception as e: logger.error(f"Error in cache cleanup: {e}") async def _load_config(self): """Load configuration from JSON file with error handling.""" try: if os.path.exists(CONFIG_FILE): try: async with aiofiles.open(CONFIG_FILE, "r", encoding="utf-8") as f: content = await f.read() if content.strip(): data = await asyncio.to_thread(json.loads, content) for guild_id_str, config in data.items(): try: guild_id = int(guild_id_str) if isinstance(config, dict): self.config_cache[guild_id] = config except (ValueError, TypeError) as e: logger.warning( f"Invalid guild ID or config in file: {guild_id_str}, {e}" ) pass else: pass except json.JSONDecodeError as e: logger.error(f"Invalid JSON in config file: {e}") backup_file = f"{CONFIG_FILE}.backup.{int(time.time())}" await asyncio.to_thread(os.rename, CONFIG_FILE, backup_file) logger.info(f"Corrupted config backed up to {backup_file}") else: pass except Exception as e: logger.error(f"Error loading config: {e}") self.config_cache = {} async def _save_config(self): """Save configuration to JSON file with atomic writes and error handling.""" async with self.config_lock: try: data = { str(guild_id): config for guild_id, config in self.config_cache.items() } content = await asyncio.to_thread(json.dumps, data, indent=2) temp_file = f"{CONFIG_FILE}.tmp" async with aiofiles.open(temp_file, "w", encoding="utf-8") as f: await f.write(content) await asyncio.to_thread(os.replace, temp_file, CONFIG_FILE) pass except Exception as e: logger.error(f"Error saving config: {e}") try: if os.path.exists(f"{CONFIG_FILE}.tmp"): await asyncio.to_thread(os.remove, f"{CONFIG_FILE}.tmp") except: pass raise async def _save_log_entry(self, guild_id: int, category: str, log_data: Dict): """Save individual log entries to JSON files with size limits and error handling.""" try: date_str = datetime.utcnow().strftime("%Y-%m-%d") log_file = Path(LOGS_DIR) / f"{guild_id}_{category}_{date_str}.json" if log_file.exists(): file_size = log_file.stat().st_size if file_size > MAX_LOG_FILE_SIZE: timestamp = datetime.utcnow().strftime("%H%M%S") log_file = ( Path(LOGS_DIR) / f"{guild_id}_{category}_{date_str}_{timestamp}.json" ) logs = [] if log_file.exists(): try: async with aiofiles.open(log_file, "r", encoding="utf-8") as f: content = await f.read() if content.strip(): logs = await asyncio.to_thread(json.loads, content) if not isinstance(logs, list): logs = [] except (json.JSONDecodeError, UnicodeDecodeError) as e: logger.warning(f"Error reading existing log file {log_file}: {e}") backup_file = f"{log_file}.backup.{int(time.time())}" await asyncio.to_thread(os.rename, log_file, backup_file) logs.append(log_data) if len(logs) > 1000: logs = logs[-1000:] temp_file = f"{log_file}.tmp" try: content = await asyncio.to_thread( json.dumps, logs, indent=2, default=str ) async with aiofiles.open(temp_file, "w", encoding="utf-8") as f: await f.write(content) await asyncio.to_thread(os.replace, temp_file, log_file) except Exception as e: logger.error(f"Error writing log file {log_file}: {e}") try: if os.path.exists(temp_file): await asyncio.to_thread(os.remove, temp_file) except: pass raise except Exception as e: logger.error(f"Error saving log entry: {e}") async def _save_log_config( self, guild_id: int, log_channels: Dict[str, Optional[int]], log_enabled: Dict[str, bool], ignore_channels: List[int], ignore_roles: List[int], ignore_users: List[int], auto_delete_duration: Optional[int], ): """Save logging configuration with validation and error handling.""" try: if not isinstance(guild_id, int) or guild_id <= 0: raise ValueError(f"Invalid guild_id: {guild_id}") if not isinstance(log_channels, dict): log_channels = {} if not isinstance(log_enabled, dict): log_enabled = {} ignore_channels = [ int(x) for x in ignore_channels if isinstance(x, (int, str)) and str(x).isdigit() ] ignore_roles = [ int(x) for x in ignore_roles if isinstance(x, (int, str)) and str(x).isdigit() ] ignore_users = [ int(x) for x in ignore_users if isinstance(x, (int, str)) and str(x).isdigit() ] guild = self.bot.get_guild(guild_id) if guild: ignore_channels = [ cid for cid in ignore_channels if guild.get_channel(cid) ] ignore_roles = [rid for rid in ignore_roles if guild.get_role(rid)] ignore_users = [uid for uid in ignore_users if guild.get_member(uid)] config = { "guild_id": str(guild_id), "log_channels": { k: v for k, v in log_channels.items() if v is not None and isinstance(v, int) }, "log_enabled": {k: bool(v) for k, v in log_enabled.items()}, "ignore_channels": list(set(ignore_channels)), "ignore_roles": list(set(ignore_roles)), "ignore_users": list(set(ignore_users)), "auto_delete_duration": auto_delete_duration if isinstance(auto_delete_duration, int) else None, "last_updated": datetime.utcnow().isoformat(), } self.config_cache[guild_id] = config await self._save_config() pass except Exception as e: logger.error(f"Error saving config for guild {guild_id}: {e}") raise async def _get_log_config(self, guild_id: int) -> Optional[Dict]: """Retrieve logging configuration from cache with validation.""" try: config = self.config_cache.get(guild_id) if config and isinstance(config, dict): return config return None except Exception as e: logger.error(f"Error retrieving config for guild {guild_id}: {e}") return None async def _get_audit_log( self, guild: discord.Guild, action: discord.AuditLogAction, target_id: Optional[int] = None, ) -> Optional[discord.AuditLogEntry]: """Fetch audit log entry with caching and proper error handling.""" if not guild.me.guild_permissions.view_audit_log: return None cache_key = (guild.id, action.value, target_id) if cache_key in self.audit_cache: entry, timestamp = self.audit_cache[cache_key] if time.time() - timestamp < 10: return entry try: async for entry in guild.audit_logs(limit=5, action=action): if target_id is None or ( hasattr(entry.target, "id") and entry.target.id == target_id ): self.audit_cache[cache_key] = (entry, time.time()) return entry except (discord.Forbidden, discord.HTTPException, discord.NotFound) as e: logger.warning(f"Failed to fetch audit log for guild {guild.id}: {e}") except Exception as e: logger.error(f"Unexpected error fetching audit log: {e}") return None async def _send_log( self, guild: discord.Guild, category: str, embed: discord.Embed, channel_id: Optional[int] = None, author_id: Optional[int] = None, ): """Send log embed with comprehensive filtering and error handling.""" try: config = await self._get_log_config(guild.id) if not config or not config.get("log_enabled", {}).get(category, False): return log_channel_id = config.get("log_channels", {}).get(category) if not log_channel_id: return channel = guild.get_channel(log_channel_id) if not channel: logger.warning( f"Log channel {log_channel_id} not found in guild {guild.id}" ) return if not channel.permissions_for(guild.me).send_messages: logger.warning( f"No permission to send messages in log channel {log_channel_id}" ) return ignore_channels = config.get("ignore_channels", []) ignore_roles = config.get("ignore_roles", []) ignore_users = config.get("ignore_users", []) auto_delete_duration = config.get("auto_delete_duration") if author_id and author_id in ignore_users: return if author_id: member = guild.get_member(author_id) if member and any(role.id in ignore_roles for role in member.roles): return if channel_id and channel_id in ignore_channels: return embed.color = 0xFF0000 delete_after = ( auto_delete_duration if auto_delete_duration and auto_delete_duration > 0 else None ) message = await channel.send( view=embed, delete_after=delete_after, allowed_mentions=None ) log_data = { "timestamp": datetime.utcnow().isoformat(), "category": category, "guild_id": guild.id, "channel_id": channel_id, "author_id": author_id, "embed_data": getattr( embed, "to_dict", lambda: {"title": "Log", "description": "CV2 Container"}, )(), "message_id": message.id, } await self._save_log_entry(guild.id, category, log_data) logger.debug( f"Sent {category} log to channel {log_channel_id} in guild {guild.id}" ) except discord.Forbidden: logger.error(f"Missing permissions to send log to channel {log_channel_id}") except discord.HTTPException as e: logger.error(f"HTTP error sending log: {e}") except Exception as e: logger.error( f"Failed to send {category} log to channel {log_channel_id}: {e}" ) def _create_modern_embed( self, title: str, description: str = None, color: int = 0xFF0000 ): """Create a modern CV2 container representation that supports Embed methods.""" embed = CV2EmbedAdapter(title, description) embed.color = color return embed def _create_status_embeds( self, guild: discord.Guild, config: Dict ) -> List[discord.Embed]: """Create comprehensive status embeds with minimal styling.""" embeds = [] main_embed = self._create_modern_embed( "Logging Configuration Status", f"Complete logging setup for {guild.name}" ) log_channels = config.get("log_channels", {}) log_enabled = config.get("log_enabled", {}) category_info = { "message_events": ("Message Events", "Message edits, deletions"), "join_leave_events": ("Join/Leave Events", "Member joins and leaves"), "member_moderation": ("Member Moderation", "Bans, timeouts, kicks"), "voice_events": ("Voice Events", "Voice channel activity"), "channel_events": ("Channel Events", "Channel create/delete/update"), "role_events": ("Role Events", "Role create/delete/update"), "emoji_events": ("Emoji Events", "Custom emoji changes"), "reaction_events": ("Reaction Events", "Message reactions"), "system_events": ("System Events", "Server updates"), } field_count = 0 for category, (name, desc) in category_info.items(): if field_count >= 25: embeds.append(main_embed) main_embed = self._create_modern_embed("Logging Status (Continued)") field_count = 0 channel = ( guild.get_channel(log_channels.get(category)) if log_channels.get(category) else None ) status = f"{channel.mention if channel else 'Not configured'}" status += ( f" {'(Enabled)' if log_enabled.get(category, False) else '(Disabled)'}" ) main_embed.add_field(name=name, value=status, inline=True) field_count += 1 main_embed.set_footer(text=f"Guild ID: {guild.id}") config_embed = self._create_modern_embed("Configuration Details") ignore_channels = config.get("ignore_channels", []) ignore_roles = config.get("ignore_roles", []) ignore_users = config.get("ignore_users", []) auto_delete = config.get("auto_delete_duration") ignored_ch = [f"<#{cid}>" for cid in ignore_channels if guild.get_channel(cid)] config_embed.add_field( name="Ignored Channels", value=", ".join(ignored_ch[:10])[:1024] if ignored_ch else "None", inline=False, ) ignored_roles_mentions = [ f"<@&{rid}>" for rid in ignore_roles if guild.get_role(rid) ] config_embed.add_field( name="Ignored Roles", value=", ".join(ignored_roles_mentions[:10])[:1024] if ignored_roles_mentions else "None", inline=False, ) ignored_users_mentions = [ f"<@{uid}>" for uid in ignore_users if guild.get_member(uid) ] config_embed.add_field( name="Ignored Users", value=", ".join(ignored_users_mentions[:10])[:1024] if ignored_users_mentions else "None", inline=False, ) duration_text = { None: "Disabled", 3600: "1 Hour", 86400: "24 Hours", 604800: "7 Days", }.get(auto_delete, f"{auto_delete} seconds") config_embed.add_field( name="Auto-Delete Duration", value=duration_text, inline=True ) last_updated = config.get("last_updated") if last_updated: try: dt = datetime.fromisoformat(last_updated.replace("Z", "+00:00")) config_embed.add_field( name="Last Updated", value=discord.utils.format_dt(dt, "R"), inline=True, ) except Exception as e: pass config_embed.set_footer(text=f"Guild ID: {guild.id}") embeds.append(main_embed) embeds.append(config_embed) return embeds @commands.hybrid_group(invoke_without_command=True, name="log") async def log(self, ctx: commands.Context): """Main logging command group. Works with both slash (/) and prefix (!) commands.""" await ctx.send_help(ctx.command) @log.command( name="setup", description="Run the interactive setup for logging channels." ) @commands.has_permissions(manage_guild=True) @commands.cooldown(1, 30, commands.BucketType.guild) @commands.max_concurrency(1, per=commands.BucketType.guild, wait=False) async def log_setup(self, ctx: commands.Context): """Enhanced interactive setup with improved user experience.""" try: config = await self._get_log_config(ctx.guild.id) if config: embed = self._create_modern_embed( "Configuration Exists", "Logging is already configured. Use /log config to modify or /log reset to start over.", ) await ctx.send(view=embed, ephemeral=True) return view = LogSetupLayoutView(self.bot, ctx.author, LOG_CATEGORIES) message = await ctx.send(view=view) except Exception as e: logger.error(f"Error in log setup: {e}") embed = self._create_modern_embed( "Setup Error", "An error occurred during setup. Please try again." ) await ctx.send(view=embed) @log.command(name="status", description="View the current logging configuration.") @commands.has_permissions(manage_guild=True) async def log_status(self, ctx: commands.Context): """Display comprehensive logging status.""" try: config = await self._get_log_config(ctx.guild.id) if not config: embed = self._create_modern_embed( "No Configuration Found", "Logging is not configured for this server. Use /log setup to get started.", f"Guild ID: {ctx.guild.id}", ) await ctx.send(view=embed) return embeds = self._create_status_embeds(ctx.guild, config) for embed in embeds: await ctx.send(view=embed) except Exception as e: logger.error(f"Error in log status: {e}") embed = self._create_modern_embed( "Status Error", "An error occurred while retrieving status." ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) @log.command( name="search", description="Search through logged events with filters." ) @commands.has_permissions(manage_guild=True) @commands.cooldown(5, 60, commands.BucketType.user) async def log_search( self, ctx: commands.Context, user: Optional[discord.Member] = None, category: Optional[str] = None, hours: Optional[int] = 24, ): """Search through logs with comprehensive filtering options.""" try: if hours and (hours < 1 or hours > 168): embed = self._create_modern_embed( "Invalid Hours", "Hours must be between 1 and 168 (1 week)." ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed, ephemeral=True) return if category and category not in LOG_CATEGORIES: embed = self._create_modern_embed( "Invalid Category", f"Valid categories: {', '.join(LOG_CATEGORIES)}" ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed, ephemeral=True) return await ctx.defer() results = [] search_date = datetime.utcnow() - timedelta(hours=hours or 24) for days_back in range((hours or 24) // 24 + 1): search_day = search_date + timedelta(days=days_back) date_str = search_day.strftime("%Y-%m-%d") categories_to_search = [category] if category else LOG_CATEGORIES for cat in categories_to_search: base_pattern = f"{ctx.guild.id}_{cat}_{date_str}" log_files = [ f for f in Path(LOGS_DIR).iterdir() if f.name.startswith(base_pattern) ] for log_file in log_files: if log_file.exists(): try: file_size = await asyncio.to_thread( log_file.stat().st_size ) if file_size > MAX_LOG_FILE_SIZE: logger.warning( f"Log file {log_file} is too large to search" ) continue async with aiofiles.open( log_file, "r", encoding="utf-8" ) as f: content = await f.read() if content.strip(): logs = await asyncio.to_thread( json.loads, content ) if isinstance(logs, list): for log_entry in logs: if not isinstance(log_entry, dict): continue try: log_time = datetime.fromisoformat( log_entry["timestamp"].replace( "Z", "+00:00" ) ) if log_time >= search_date: if ( not user or log_entry.get( "author_id" ) == user.id ): results.append(log_entry) if ( len(results) >= MAX_SEARCH_RESULTS ): break except (KeyError, ValueError): continue except Exception as e: logger.error(f"Error reading log file {log_file}: {e}") continue if len(results) >= MAX_SEARCH_RESULTS: break if len(results) >= MAX_SEARCH_RESULTS: break if not results: embed = self._create_modern_embed( "No Results Found", "No log entries match your search criteria." ) # embed.set_footer removed - CV2 doesn't support it await ctx.followup.send(view=embed) return results.sort(key=lambda x: x.get("timestamp", ""), reverse=True) results = results[:50] embed = self._create_modern_embed( f"Search Results ({len(results)} found)", f"Showing logs from the last {hours} hours", ) if user: embed.description += f" for {user.mention}" if category: embed.description += f" in category {category}" for i, result in enumerate(results[:10]): try: timestamp = datetime.fromisoformat( result["timestamp"].replace("Z", "+00:00") ) category_name = ( result.get("category", "Unknown").replace("_", " ").title() ) channel_id = result.get("channel_id") channel_text = f"<#{channel_id}>" if channel_id else "Unknown" embed.add_field( name=f"{i + 1}. {category_name}", value=f"{discord.utils.format_dt(timestamp, 'R')} • {channel_text}", inline=False, ) except Exception as e: continue if len(results) > 10: embed.set_footer( text=f"Showing 10 of {len(results)} results • Guild ID: {ctx.guild.id}" ) else: pass await ctx.followup.send(view=embed) except Exception as e: logger.error(f"Error in log search: {e}") embed = self._create_modern_embed( "Search Error", "An error occurred while searching logs." ) # embed.set_footer removed - CV2 doesn't support it try: await ctx.followup.send(view=embed) except: await ctx.send(view=embed, ephemeral=True) @log.command(name="reset", description="Reset all logging configuration.") @commands.has_permissions(manage_guild=True) async def log_reset(self, ctx: commands.Context): """Reset logging configuration for the guild.""" try: if ctx.guild.id in self.config_cache: del self.config_cache[ctx.guild.id] await self._save_config() embed = self._create_modern_embed( "Configuration Reset", "All logging configuration has been cleared. Use /log setup to reconfigure.", ) else: embed = self._create_modern_embed( "No Configuration Found", "There is no logging configuration to reset.", ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) except Exception as e: logger.error(f"Error in log reset: {e}") embed = self._create_modern_embed( "Reset Error", "An error occurred while resetting configuration." ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) @log.command( name="toggle", description="Enable or disable specific logging categories." ) @commands.has_permissions(manage_guild=True) async def log_toggle(self, ctx: commands.Context, category: str, enabled: bool): """Toggle logging categories on/off.""" try: if category not in LOG_CATEGORIES: embed = self._create_modern_embed( "Invalid Category", f"Valid categories: {', '.join(LOG_CATEGORIES)}" ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed, ephemeral=True) return config = await self._get_log_config(ctx.guild.id) if not config: embed = self._create_modern_embed( "No Configuration Found", "Please run /log setup first to configure logging.", ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) return log_enabled = config.get("log_enabled", {}) log_enabled[category] = enabled await self._save_log_config( ctx.guild.id, config.get("log_channels", {}), log_enabled, config.get("ignore_channels", []), config.get("ignore_roles", []), config.get("ignore_users", []), config.get("auto_delete_duration"), ) status = "enabled" if enabled else "disabled" embed = self._create_modern_embed( f"Category {status.title()}", f"{category.replace('_', ' ').title()} logging has been {status}.", ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) except Exception as e: logger.error(f"Error in log toggle: {e}") embed = self._create_modern_embed( "Toggle Error", "An error occurred while toggling category." ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) @log.command( name="config", description="Open interactive config menu for logging settings." ) @commands.has_permissions(manage_guild=True) async def log_config(self, ctx: commands.Context): """Fully interactive configuration menu.""" try: config = await self._get_log_config(ctx.guild.id) if not config: embed = self._create_modern_embed( "No Configuration Found", "Please run /log setup first to configure logging.", ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) return view = InteractiveConfigView(self.bot, ctx.author, config) embed = self._create_modern_embed( "Logging Configuration", "Use the buttons below to modify your logging settings.", ) embed.add_field( name="Available Options", value=( "Change Channels - Configure log channels for categories\n" "Toggle Categories - Enable/disable logging categories\n" "Manage Ignores - Manage ignore lists\n" "Auto-Delete Settings - Set auto-delete duration" ), inline=False, ) # embed.set_footer removed - CV2 doesn't support it from discord.ui import ActionRow add_action_rows(embed, view.children) message = await ctx.send(view=embed) view.message = message except Exception as e: logger.error(f"Error in log config: {e}") embed = self._create_modern_embed( "Config Error", "An error occurred while loading configuration." ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) @log.command(name="ignore", description="Manage ignored channels, roles, or users.") @commands.has_permissions(manage_guild=True) async def log_ignore( self, ctx: commands.Context, action: str = "list", target_type: str = None, target: str = None, ): """Manage ignore lists.""" try: if action not in ["add", "remove", "list", "clear"]: embed = self._create_modern_embed( "Invalid Action", "Valid actions: add, remove, list, clear" ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed, ephemeral=True) return config = await self._get_log_config(ctx.guild.id) if not config: embed = self._create_modern_embed( "No Configuration Found", "Please run /log setup first to configure logging.", ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) return if action == "list": embed = self._create_modern_embed("Current Ignore Lists") ignore_channels = config.get("ignore_channels", []) ignore_roles = config.get("ignore_roles", []) ignore_users = config.get("ignore_users", []) channels_text = ( "\n".join( [ f"<#{cid}>" for cid in ignore_channels if ctx.guild.get_channel(cid) ] ) or "None" ) roles_text = ( "\n".join( [ f"<@&{rid}>" for rid in ignore_roles if ctx.guild.get_role(rid) ] ) or "None" ) users_text = ( "\n".join( [ f"<@{uid}>" for uid in ignore_users if ctx.guild.get_member(uid) ] ) or "None" ) embed.add_field( name="Ignored Channels", value=channels_text[:1024], inline=False ) embed.add_field( name="Ignored Roles", value=roles_text[:1024], inline=False ) embed.add_field( name="Ignored Users", value=users_text[:1024], inline=False ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) return if not target_type or not target: embed = self._create_modern_embed( "Missing Parameters", f"Usage: /log ignore {action} ", ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed, ephemeral=True) return ignore_channels = config.get("ignore_channels", []) ignore_roles = config.get("ignore_roles", []) ignore_users = config.get("ignore_users", []) target_obj = None target_id = None if target_type.lower() == "channel": if target.startswith("<#") and target.endswith(">"): target_id = int(target[2:-1]) target_obj = ctx.guild.get_channel(target_id) elif target.isdigit(): target_id = int(target) target_obj = ctx.guild.get_channel(target_id) else: target_obj = discord.utils.get(ctx.guild.text_channels, name=target) target_id = target_obj.id if target_obj else None if not target_obj: embed = self._create_modern_embed( "Channel Not Found", "Could not find the specified channel." ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed, ephemeral=True) return target_list = ignore_channels target_name = f"#{target_obj.name}" elif target_type.lower() == "role": if target.startswith("<@&") and target.endswith(">"): target_id = int(target[3:-1]) target_obj = ctx.guild.get_role(target_id) elif target.isdigit(): target_id = int(target) target_obj = ctx.guild.get_role(target_id) else: target_obj = discord.utils.get(ctx.guild.roles, name=target) target_id = target_obj.id if target_obj else None if not target_obj: embed = self._create_modern_embed( "Role Not Found", "Could not find the specified role." ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed, ephemeral=True) return target_list = ignore_roles target_name = f"@{target_obj.name}" elif target_type.lower() == "user": if target.startswith("<@") and target.endswith(">"): target_id = int(target[2:-1].replace("!", "")) target_obj = ctx.guild.get_member(target_id) elif target.isdigit(): target_id = int(target) target_obj = ctx.guild.get_member(target_id) else: target_obj = discord.utils.get( ctx.guild.members, display_name=target ) if not target_obj: target_obj = discord.utils.get(ctx.guild.members, name=target) target_id = target_obj.id if target_obj else None if not target_obj: embed = self._create_modern_embed( "User Not Found", "Could not find the specified user." ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed, ephemeral=True) return target_list = ignore_users target_name = f"{target_obj.display_name}" else: embed = self._create_modern_embed( "Invalid Target Type", "Target type must be 'channel', 'role', or 'user'.", ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed, ephemeral=True) return if action == "add": if target_id not in target_list: target_list.append(target_id) message = f"Added {target_name} to ignore list." else: message = f"{target_name} is already in the ignore list." elif action == "remove": if target_id in target_list: target_list.remove(target_id) message = f"Removed {target_name} from ignore list." else: message = f"{target_name} is not in the ignore list." await self._save_log_config( ctx.guild.id, config.get("log_channels", {}), config.get("log_enabled", {}), ignore_channels, ignore_roles, ignore_users, config.get("auto_delete_duration"), ) embed = self._create_modern_embed("Ignore List Updated", message) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) except Exception as e: logger.error(f"Error in log ignore: {e}") embed = self._create_modern_embed( "Ignore Error", "An error occurred while managing ignore lists." ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) @log.command( name="test", description="Send test log messages to configured channels." ) @commands.has_permissions(manage_guild=True) async def log_test(self, ctx: commands.Context, category: str = None): """Send test log messages.""" try: config = await self._get_log_config(ctx.guild.id) if not config: embed = self._create_modern_embed( "No Configuration Found", "Please run /log setup first to configure logging.", ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) return if category and category not in LOG_CATEGORIES: embed = self._create_modern_embed( "Invalid Category", f"Valid categories: {', '.join(LOG_CATEGORIES)}" ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed, ephemeral=True) return categories_to_test = ( [category] if category and category in LOG_CATEGORIES else LOG_CATEGORIES ) sent_count = 0 for cat in categories_to_test: if config.get("log_enabled", {}).get(cat, False): embed = self._create_modern_embed( f"Test Log - {cat.replace('_', ' ').title()}" ) embed.add_field(name="Category", value=cat, inline=True) embed.add_field( name="Test User", value=ctx.author.mention, inline=True ) embed.add_field( name="Timestamp", value=discord.utils.format_dt(datetime.utcnow(), "f"), inline=True, ) embed.set_footer(text=f"Test message • User ID: {ctx.author.id}") await self._send_log( ctx.guild, cat, embed, ctx.channel.id, ctx.author.id ) sent_count += 1 if sent_count > 0: embed = self._create_modern_embed( "Test Messages Sent", f"Sent {sent_count} test log messages to configured channels.", ) else: embed = self._create_modern_embed( "No Test Messages Sent", "No logging categories are enabled or configured.", ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) except Exception as e: logger.error(f"Error in log test: {e}") embed = self._create_modern_embed( "Test Error", "An error occurred while sending test messages." ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed) @log.command(name="export", description="Export log data as a JSON file.") @commands.has_permissions(manage_guild=True) @commands.cooldown(1, 300, commands.BucketType.guild) async def log_export(self, ctx: commands.Context, days: int = 7): """Export log data for the specified number of days.""" try: if days < 1 or days > 30: embed = self._create_modern_embed( "Invalid Range", "Days must be between 1 and 30." ) # embed.set_footer removed - CV2 doesn't support it await ctx.send(view=embed, ephemeral=True) return await ctx.defer() exported_data = {} total_entries = 0 for day_offset in range(days): date = datetime.utcnow() - timedelta(days=day_offset) date_str = date.strftime("%Y-%m-%d") for category in LOG_CATEGORIES: base_pattern = f"{ctx.guild.id}_{category}_{date_str}" log_files = [ f for f in Path(LOGS_DIR).iterdir() if f.name.startswith(base_pattern) ] for log_file in log_files: if log_file.exists(): try: async with aiofiles.open( log_file, "r", encoding="utf-8" ) as f: content = await f.read() if content.strip(): logs = await asyncio.to_thread( json.loads, content ) if isinstance(logs, list) and logs: key = f"{date_str}_{category}_{log_file.name.split('_')[-1].split('.')[0]}" exported_data[key] = logs total_entries += len(logs) except Exception as e: logger.error(f"Error reading log file {log_file}: {e}") if not exported_data: embed = self._create_modern_embed( "No Data Found", f"No log data found for the last {days} days." ) # embed.set_footer removed - CV2 doesn't support it await ctx.followup.send(view=embed) return export_filename = f"logs_export_{ctx.guild.id}_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.json" export_data = { "guild_id": ctx.guild.id, "guild_name": ctx.guild.name, "export_date": datetime.utcnow().isoformat(), "days_exported": days, "total_entries": total_entries, "data": exported_data, } temp_path = Path(LOGS_DIR) / export_filename content = await asyncio.to_thread( json.dumps, export_data, indent=2, default=str ) async with aiofiles.open(temp_path, "w", encoding="utf-8") as f: await f.write(content) embed = self._create_modern_embed( "Export Complete", f"Exported {total_entries} log entries from the last {days} days.", ) embed.add_field( name="File Size", value=f"{len(content.encode('utf-8')) / 1024:.2f} KB", inline=True, ) embed.add_field( name="Categories", value=str(len(set(k.split("_", 1)[1] for k in exported_data.keys()))), inline=True, ) # embed.set_footer removed - CV2 doesn't support it with open(temp_path, "rb") as f: file = discord.File(f, filename=export_filename) await ctx.followup.send(view=embed, file=file) await asyncio.to_thread(temp_path.unlink) except Exception as e: logger.error(f"Error exporting logs: {e}") embed = self._create_modern_embed( "Export Failed", "An error occurred while exporting log data." ) # embed.set_footer removed - CV2 doesn't support it try: await ctx.followup.send(view=embed) except: await ctx.send(view=embed) @commands.Cog.listener() async def on_message_edit(self, before: discord.Message, after: discord.Message): """Enhanced message edit logging with content comparison.""" try: if not before.guild or before.author.bot or before.content == after.content: return embed = self._create_modern_embed("Message Edited") embed.set_author( name=before.author.display_name, icon_url=before.author.display_avatar.url, ) before_content = before.content[:1000] if before.content else "No content" after_content = after.content[:1000] if after.content else "No content" embed.add_field( name="Before", value=f"```{before_content}```", inline=False ) embed.add_field(name="After", value=f"```{after_content}```", inline=False) embed.add_field(name="Channel", value=before.channel.mention, inline=True) embed.add_field(name="User", value=f"{before.author.mention}", inline=True) embed.add_field( name="Jump to Message", value=f"[Click here]({after.jump_url})", inline=True, ) embed.set_footer( text=f"User ID: {before.author.id} • Message ID: {before.id}" ) await self._send_log( before.guild, "message_events", embed, before.channel.id, before.author.id, ) except Exception as e: logger.error(f"Error in on_message_edit: {e}") @commands.Cog.listener() async def on_message_delete(self, message: discord.Message): """Enhanced message deletion logging with attachment info.""" try: if not message.guild or message.author.bot: return embed = self._create_modern_embed("Message Deleted") embed.set_author( name=message.author.display_name, icon_url=message.author.display_avatar.url, ) content = message.content[:1000] if message.content else "No content" embed.add_field(name="Content", value=f"```{content}```", inline=False) embed.add_field(name="Channel", value=message.channel.mention, inline=True) embed.add_field(name="User", value=message.author.mention, inline=True) if message.attachments: attachments = "\n".join( [f"• {att.filename}" for att in message.attachments[:5]] ) embed.add_field(name="Attachments", value=attachments, inline=False) embed.set_footer( text=f"User ID: {message.author.id} • Message ID: {message.id}" ) await self._send_log( message.guild, "message_events", embed, message.channel.id, message.author.id, ) except Exception as e: logger.error(f"Error in on_message_delete: {e}") @commands.Cog.listener() async def on_member_join(self, member: discord.Member): """Log member joins with comprehensive information.""" try: embed = self._create_modern_embed("Member Joined") embed.set_author( name=member.display_name, icon_url=member.display_avatar.url ) embed.add_field(name="User", value=member.mention, inline=True) embed.add_field( name="Account Created", value=discord.utils.format_dt(member.created_at, "R"), inline=True, ) embed.add_field( name="Member Count", value=str(member.guild.member_count), inline=True ) embed.set_footer(text=f"User ID: {member.id}") embed.set_thumbnail(url=member.display_avatar.url) await self._send_log( member.guild, "join_leave_events", embed, None, member.id ) except Exception as e: logger.error(f"Error in on_member_join: {e}") @commands.Cog.listener() async def on_member_remove(self, member: discord.Member): """Log member leaves with role information.""" try: embed = self._create_modern_embed("Member Left") embed.set_author( name=member.display_name, icon_url=member.display_avatar.url ) embed.add_field( name="User", value=f"{member.mention} ({member})", inline=True ) embed.add_field( name="Joined", value=discord.utils.format_dt(member.joined_at, "R") if member.joined_at else "Unknown", inline=True, ) embed.add_field( name="Member Count", value=str(member.guild.member_count), inline=True ) if member.roles[1:]: roles = ", ".join([role.mention for role in member.roles[1:][:10]]) embed.add_field(name="Roles", value=roles, inline=False) embed.set_footer(text=f"User ID: {member.id}") embed.set_thumbnail(url=member.display_avatar.url) await self._send_log( member.guild, "join_leave_events", embed, None, member.id ) except Exception as e: logger.error(f"Error in on_member_remove: {e}") @commands.Cog.listener() async def on_member_update(self, before: discord.Member, after: discord.Member): """Log member updates including role changes.""" try: if before.roles != after.roles: embed = self._create_modern_embed("Member Roles Updated") embed.set_author( name=after.display_name, icon_url=after.display_avatar.url ) embed.add_field(name="User", value=after.mention, inline=True) added_roles = set(after.roles) - set(before.roles) removed_roles = set(before.roles) - set(after.roles) if added_roles: roles_added = ", ".join([role.mention for role in added_roles]) embed.add_field(name="Roles Added", value=roles_added, inline=False) if removed_roles: roles_removed = ", ".join([role.mention for role in removed_roles]) embed.add_field( name="Roles Removed", value=roles_removed, inline=False ) embed.set_footer(text=f"User ID: {after.id}") await self._send_log( after.guild, "member_moderation", embed, None, after.id ) if before.nick != after.nick: embed = self._create_modern_embed("Nickname Changed") embed.set_author( name=after.display_name, icon_url=after.display_avatar.url ) embed.add_field(name="User", value=after.mention, inline=True) embed.add_field( name="Before", value=before.nick or "No nickname", inline=True ) embed.add_field( name="After", value=after.nick or "No nickname", inline=True ) embed.set_footer(text=f"User ID: {after.id}") await self._send_log( after.guild, "member_moderation", embed, None, after.id ) except Exception as e: logger.error(f"Error in on_member_update: {e}") @commands.Cog.listener() async def on_member_ban( self, guild: discord.Guild, user: Union[discord.User, discord.Member] ): """Log member bans with audit log information.""" try: embed = self._create_modern_embed("Member Banned") embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) embed.add_field(name="User", value=f"{user.mention} ({user})", inline=True) audit_entry = await self._get_audit_log( guild, discord.AuditLogAction.ban, user.id ) if audit_entry: embed.add_field( name="Banned by", value=audit_entry.user.mention, inline=True ) if audit_entry.reason: embed.add_field( name="Reason", value=audit_entry.reason[:1024], inline=False ) embed.set_footer(text=f"User ID: {user.id}") embed.set_thumbnail(url=user.display_avatar.url) await self._send_log(guild, "member_moderation", embed, None, user.id) except Exception as e: logger.error(f"Error in on_member_ban: {e}") @commands.Cog.listener() async def on_member_unban(self, guild: discord.Guild, user: discord.User): """Log member unbans.""" try: embed = self._create_modern_embed("Member Unbanned") embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) embed.add_field(name="User", value=f"{user.mention} ({user})", inline=True) audit_entry = await self._get_audit_log( guild, discord.AuditLogAction.unban, user.id ) if audit_entry: embed.add_field( name="Unbanned by", value=audit_entry.user.mention, inline=True ) if audit_entry.reason: embed.add_field( name="Reason", value=audit_entry.reason[:1024], inline=False ) embed.set_footer(text=f"User ID: {user.id}") embed.set_thumbnail(url=user.display_avatar.url) await self._send_log(guild, "member_moderation", embed, None, user.id) except Exception as e: logger.error(f"Error in on_member_unban: {e}") @commands.Cog.listener() async def on_voice_state_update( self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState, ): """Log voice state changes.""" try: if before.channel == after.channel: return embed = self._create_modern_embed("Voice State Changed") embed.set_author( name=member.display_name, icon_url=member.display_avatar.url ) embed.add_field(name="User", value=member.mention, inline=True) if before.channel and after.channel: embed.add_field(name="Action", value="Moved channels", inline=True) embed.add_field(name="From", value=before.channel.mention, inline=True) embed.add_field(name="To", value=after.channel.mention, inline=True) elif after.channel: embed.add_field(name="Action", value="Joined voice", inline=True) embed.add_field( name="Channel", value=after.channel.mention, inline=True ) elif before.channel: embed.add_field(name="Action", value="Left voice", inline=True) embed.add_field( name="Channel", value=before.channel.mention, inline=True ) embed.set_footer(text=f"User ID: {member.id}") await self._send_log(member.guild, "voice_events", embed, None, member.id) except Exception as e: logger.error(f"Error in on_voice_state_update: {e}") @commands.Cog.listener() async def on_guild_channel_create(self, channel): """Log channel creation.""" try: embed = self._create_modern_embed("Channel Created") embed.add_field(name="Channel", value=channel.mention, inline=True) embed.add_field(name="Type", value=str(channel.type).title(), inline=True) embed.add_field( name="Category", value=channel.category.name if channel.category else "None", inline=True, ) audit_entry = await self._get_audit_log( channel.guild, discord.AuditLogAction.channel_create, channel.id ) if audit_entry: embed.add_field( name="Created by", value=audit_entry.user.mention, inline=True ) embed.set_footer(text=f"Channel ID: {channel.id}") await self._send_log(channel.guild, "channel_events", embed, channel.id) except Exception as e: logger.error(f"Error in on_guild_channel_create: {e}") @commands.Cog.listener() async def on_guild_channel_delete(self, channel): """Log channel deletion.""" try: embed = self._create_modern_embed("Channel Deleted") embed.add_field(name="Channel", value=f"#{channel.name}", inline=True) embed.add_field(name="Type", value=str(channel.type).title(), inline=True) embed.add_field( name="Category", value=channel.category.name if channel.category else "None", inline=True, ) audit_entry = await self._get_audit_log( channel.guild, discord.AuditLogAction.channel_delete, channel.id ) if audit_entry: embed.add_field( name="Deleted by", value=audit_entry.user.mention, inline=True ) embed.set_footer(text=f"Channel ID: {channel.id}") await self._send_log(channel.guild, "channel_events", embed, channel.id) except Exception as e: logger.error(f"Error in on_guild_channel_delete: {e}") @commands.Cog.listener() async def on_guild_channel_update(self, before, after): """Log channel updates.""" try: changes = [] if before.name != after.name: changes.append(f"Name: {before.name} → {after.name}") if getattr(before, "topic", None) != getattr(after, "topic", None): changes.append( f"Topic: {before.topic or 'None'} → {after.topic or 'None'}" ) if before.category != after.category: before_cat = before.category.name if before.category else "None" after_cat = after.category.name if after.category else "None" changes.append(f"Category: {before_cat} → {after_cat}") if not changes: return embed = self._create_modern_embed("Channel Updated") embed.add_field(name="Channel", value=after.mention, inline=True) embed.add_field(name="Changes", value="\n".join(changes), inline=False) audit_entry = await self._get_audit_log( after.guild, discord.AuditLogAction.channel_update, after.id ) if audit_entry: embed.add_field( name="Updated by", value=audit_entry.user.mention, inline=True ) embed.set_footer(text=f"Channel ID: {after.id}") await self._send_log(after.guild, "channel_events", embed, after.id) except Exception as e: logger.error(f"Error in on_guild_channel_update: {e}") @commands.Cog.listener() async def on_guild_role_create(self, role): """Log role creation.""" try: embed = self._create_modern_embed("Role Created") embed.add_field(name="Role", value=role.mention, inline=True) embed.add_field(name="Color", value=str(role.color), inline=True) embed.add_field( name="Mentionable", value="Yes" if role.mentionable else "No", inline=True, ) audit_entry = await self._get_audit_log( role.guild, discord.AuditLogAction.role_create, role.id ) if audit_entry: embed.add_field( name="Created by", value=audit_entry.user.mention, inline=True ) embed.set_footer(text=f"Role ID: {role.id}") await self._send_log(role.guild, "role_events", embed) except Exception as e: logger.error(f"Error in on_guild_role_create: {e}") @commands.Cog.listener() async def on_guild_role_delete(self, role): """Log role deletion.""" try: embed = self._create_modern_embed("Role Deleted") embed.add_field(name="Role", value=f"@{role.name}", inline=True) embed.add_field(name="Color", value=str(role.color), inline=True) embed.add_field(name="Members", value=str(len(role.members)), inline=True) audit_entry = await self._get_audit_log( role.guild, discord.AuditLogAction.role_delete, role.id ) if audit_entry: embed.add_field( name="Deleted by", value=audit_entry.user.mention, inline=True ) embed.set_footer(text=f"Role ID: {role.id}") await self._send_log(role.guild, "role_events", embed) except Exception as e: logger.error(f"Error in on_guild_role_delete: {e}") @commands.Cog.listener() async def on_guild_role_update(self, before, after): """Log role updates.""" try: changes = [] if before.name != after.name: changes.append(f"Name: {before.name} → {after.name}") if before.color != after.color: changes.append(f"Color: {before.color} → {after.color}") if before.mentionable != after.mentionable: changes.append( f"Mentionable: {before.mentionable} → {after.mentionable}" ) if before.permissions != after.permissions: changes.append("Permissions updated") if not changes: return embed = self._create_modern_embed("Role Updated") embed.add_field(name="Role", value=after.mention, inline=True) embed.add_field(name="Changes", value="\n".join(changes), inline=False) audit_entry = await self._get_audit_log( after.guild, discord.AuditLogAction.role_update, after.id ) if audit_entry: embed.add_field( name="Updated by", value=audit_entry.user.mention, inline=True ) embed.set_footer(text=f"Role ID: {after.id}") await self._send_log(after.guild, "role_events", embed) except Exception as e: logger.error(f"Error in on_guild_role_update: {e}") @commands.Cog.listener() async def on_guild_update(self, before, after): """Log guild/server updates.""" try: changes = [] if before.name != after.name: changes.append(f"Name: {before.name} → {after.name}") if before.description != after.description: changes.append(f"Description updated") if before.verification_level != after.verification_level: changes.append( f"Verification Level: {before.verification_level} → {after.verification_level}" ) if not changes: return embed = self._create_modern_embed("Server Updated") embed.add_field(name="Changes", value="\n".join(changes), inline=False) audit_entry = await self._get_audit_log( after, discord.AuditLogAction.guild_update ) if audit_entry: embed.add_field( name="Updated by", value=audit_entry.user.mention, inline=True ) embed.set_footer(text=f"Guild ID: {after.id}") await self._send_log(after, "system_events", embed) except Exception as e: logger.error(f"Error in on_guild_update: {e}") @commands.Cog.listener() async def on_guild_emojis_update(self, guild, before, after): """Log emoji updates.""" try: added_emojis = set(after) - set(before) removed_emojis = set(before) - set(after) if added_emojis: for emoji in added_emojis: embed = self._create_modern_embed("Emoji Added") embed.add_field( name="Emoji", value=f"{emoji} :{emoji.name}:", inline=True ) embed.add_field(name="ID", value=str(emoji.id), inline=True) embed.add_field( name="Animated", value="Yes" if emoji.animated else "No", inline=True, ) audit_entry = await self._get_audit_log( guild, discord.AuditLogAction.emoji_create, emoji.id ) if audit_entry: embed.add_field( name="Created by", value=audit_entry.user.mention, inline=True, ) embed.set_footer(text=f"Emoji ID: {emoji.id}") await self._send_log(guild, "emoji_events", embed) if removed_emojis: for emoji in removed_emojis: embed = self._create_modern_embed("Emoji Removed") embed.add_field(name="Emoji", value=f":{emoji.name}:", inline=True) embed.add_field(name="ID", value=str(emoji.id), inline=True) embed.add_field( name="Animated", value="Yes" if emoji.animated else "No", inline=True, ) audit_entry = await self._get_audit_log( guild, discord.AuditLogAction.emoji_delete, emoji.id ) if audit_entry: embed.add_field( name="Deleted by", value=audit_entry.user.mention, inline=True, ) embed.set_footer(text=f"Emoji ID: {emoji.id}") await self._send_log(guild, "emoji_events", embed) except Exception as e: logger.error(f"Error in on_guild_emojis_update: {e}") @commands.Cog.listener() async def on_reaction_add(self, reaction, user): """Log reaction additions (optional).""" try: if not reaction.message.guild or user.bot: return embed = self._create_modern_embed("Reaction Added") embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) embed.add_field(name="User", value=user.mention, inline=True) embed.add_field(name="Reaction", value=str(reaction.emoji), inline=True) embed.add_field( name="Message", value=f"[Jump to message]({reaction.message.jump_url})", inline=True, ) embed.add_field( name="Channel", value=reaction.message.channel.mention, inline=True ) embed.set_footer( text=f"User ID: {user.id} • Message ID: {reaction.message.id}" ) await self._send_log( reaction.message.guild, "reaction_events", embed, reaction.message.channel.id, user.id, ) except Exception as e: logger.error(f"Error in on_reaction_add: {e}") @commands.Cog.listener() async def on_reaction_remove(self, reaction, user): """Log reaction removals (optional).""" try: if not reaction.message.guild or user.bot: return embed = self._create_modern_embed("Reaction Removed") embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) embed.add_field(name="User", value=user.mention, inline=True) embed.add_field(name="Reaction", value=str(reaction.emoji), inline=True) embed.add_field( name="Message", value=f"[Jump to message]({reaction.message.jump_url})", inline=True, ) embed.add_field( name="Channel", value=reaction.message.channel.mention, inline=True ) embed.set_footer( text=f"User ID: {user.id} • Message ID: {reaction.message.id}" ) await self._send_log( reaction.message.guild, "reaction_events", embed, reaction.message.channel.id, user.id, ) except Exception as e: logger.error(f"Error in on_reaction_remove: {e}") async def setup(bot): """Setup function for the cog.""" await bot.add_cog(Logging(bot))