import discord from redbot.core import commands, Config import datetime from typing import Dict, Optional, Union class Logging(commands.Cog): """ A cog for comprehensive server event logging. """ def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=1234567893, force_registration=True) default_guild = { "log_channel": None, "logged_events": {}, # For DataManager "enabled_events": { "on_message_delete": True, "on_message_edit": True, "on_member_join": True, "on_member_remove": True, "on_member_update": True, "on_voice_state_update": True, "on_invite_create": True, }, } self.config.register_guild(**default_guild) async def _send_log(self, guild: discord.Guild, embed: discord.Embed, event_name: str): """Helper function to send logs.""" enabled_events = await self.config.guild(guild).enabled_events() if not enabled_events.get(event_name, False): return log_channel_id = await self.config.guild(guild).log_channel() if not log_channel_id: return log_channel = guild.get_channel(log_channel_id) if isinstance(log_channel, discord.TextChannel): try: log_message = await log_channel.send(embed=embed) if event_name in ["on_message_delete", "on_message_edit"]: async with self.config.guild(guild).logged_events() as events: events[str(log_message.id)] = datetime.datetime.now( datetime.timezone.utc ).isoformat() except discord.Forbidden: pass # Bot lacks permissions # --- SETTINGS COMMANDS --- @commands.group(aliases=["logset"]) # type: ignore @commands.guild_only() @commands.admin_or_permissions(manage_guild=True) async def loggingset(self, ctx: commands.Context): """Configure logging settings.""" pass @loggingset.command(name="channel") async def set_log_channel(self, ctx: commands.Context, channel: discord.TextChannel): """Set the channel where logs will be sent.""" if not ctx.guild: return await self.config.guild(ctx.guild).log_channel.set(channel.id) await ctx.send(f"Log channel has been set to {channel.mention}") @loggingset.command(name="toggle") async def toggle_log_event(self, ctx: commands.Context, event: str): """Toggle logging for a specific event.""" if not ctx.guild: return valid_events = list((await self.config.guild(ctx.guild).enabled_events()).keys()) if event not in valid_events: await ctx.send(f"Invalid event. Valid events are: `{'`, `'.join(valid_events)}`") return async with self.config.guild(ctx.guild).enabled_events() as events: current_status = events.get(event, False) events[event] = not current_status new_status = "enabled" if not current_status else "disabled" await ctx.send(f"Logging for `{event}` has been {new_status}.") # --- EVENT LISTENERS --- @commands.Cog.listener() async def on_message_delete(self, message: discord.Message): """Log when a message is deleted.""" if message.author.bot or not message.guild: return location = "" if isinstance(message.channel, (discord.TextChannel, discord.Thread)): location = message.channel.mention elif isinstance(message.channel, discord.DMChannel): location = f"DM with {message.channel.recipient}" elif isinstance(message.channel, discord.GroupChannel): location = f"Group DM ({message.channel.id})" else: location = f"Channel ID: {message.channel.id}" embed = discord.Embed( description=f"**Message deleted in {location}**\n{message.content or 'No text content.'}", color=discord.Color.red(), timestamp=datetime.datetime.now(datetime.timezone.utc), ) embed.set_author( name=f"{message.author.name} ({message.author.id})", icon_url=message.author.display_avatar.url, ) await self._send_log(message.guild, embed, "on_message_delete") @commands.Cog.listener() async def on_message_edit(self, before: discord.Message, after: discord.Message): """Log when a message is edited.""" if before.author.bot or not before.guild or before.content == after.content: return location = "" if isinstance(before.channel, (discord.TextChannel, discord.Thread)): location = before.channel.mention elif isinstance(before.channel, discord.DMChannel): location = f"DM with {before.channel.recipient}" elif isinstance(before.channel, discord.GroupChannel): location = f"Group DM ({before.channel.id})" else: location = f"Channel ID: {before.channel.id}" embed = discord.Embed( description=f"**Message edited in {location}** [Jump to Message]({after.jump_url})", color=discord.Color.orange(), timestamp=datetime.datetime.now(datetime.timezone.utc), ) embed.set_author( name=f"{before.author.name} ({before.author.id})", icon_url=before.author.display_avatar.url, ) embed.add_field(name="Before", value=before.content or "Empty", inline=False) embed.add_field(name="After", value=after.content or "Empty", inline=False) await self._send_log(before.guild, embed, "on_message_edit") @commands.Cog.listener() async def on_member_join(self, member: discord.Member): """Log when a user joins the server.""" embed = discord.Embed( description=f"**{member.mention} has joined the server.**", color=discord.Color.green(), timestamp=datetime.datetime.now(datetime.timezone.utc), ) embed.set_author( name=f"{member.name} ({member.id})", icon_url=member.display_avatar.url ) await self._send_log(member.guild, embed, "on_member_join") @commands.Cog.listener() async def on_member_remove(self, member: discord.Member): """Log when a user leaves the server.""" embed = discord.Embed( description=f"**{member.mention} has left the server.**", color=discord.Color.dark_red(), timestamp=datetime.datetime.now(datetime.timezone.utc), ) embed.set_author( name=f"{member.name} ({member.id})", icon_url=member.display_avatar.url ) await self._send_log(member.guild, embed, "on_member_remove") @commands.Cog.listener() async def on_member_update(self, before: discord.Member, after: discord.Member): """Log when a member's roles or nickname changes.""" guild = after.guild if before.nick != after.nick: embed = discord.Embed( description=f"**{after.mention} changed their nickname.**", color=discord.Color.blue(), timestamp=datetime.datetime.now(datetime.timezone.utc), ) embed.set_author( name=f"{after.name} ({after.id})", icon_url=after.display_avatar.url ) embed.add_field(name="Before", value=before.nick or "None", inline=False) embed.add_field(name="After", value=after.nick or "None", inline=False) await self._send_log(guild, embed, "on_member_update") if before.roles != after.roles: added_roles = [r.mention for r in after.roles if r not in before.roles] removed_roles = [r.mention for r in before.roles if r not in after.roles] if added_roles or removed_roles: embed = discord.Embed( description=f"**{after.mention}'s roles were updated.**", color=discord.Color.blue(), timestamp=datetime.datetime.now(datetime.timezone.utc), ) embed.set_author( name=f"{after.name} ({after.id})", icon_url=after.display_avatar.url, ) if added_roles: embed.add_field( name="Added Roles", value=", ".join(added_roles), inline=False ) if removed_roles: embed.add_field( name="Removed Roles", value=", ".join(removed_roles), inline=False, ) await self._send_log(guild, embed, "on_member_update") @commands.Cog.listener() async def on_voice_state_update( self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState ): """Log when a user's voice state changes.""" if before.channel == after.channel: return # Ignore mutes, deafens, etc. for now guild = member.guild action = "" if before.channel and not after.channel: action = f"left voice channel {before.channel.mention}." elif not before.channel and after.channel: action = f"joined voice channel {after.channel.mention}." elif before.channel and after.channel: action = f"moved from {before.channel.mention} to {after.channel.mention}." if action: embed = discord.Embed( description=f"**{member.mention} {action}**", color=discord.Color.light_grey(), timestamp=datetime.datetime.now(datetime.timezone.utc), ) embed.set_author( name=f"{member.name} ({member.id})", icon_url=member.display_avatar.url ) await self._send_log(guild, embed, "on_voice_state_update") @commands.Cog.listener() async def on_invite_create(self, invite: discord.Invite): """Log when an invite is created.""" guild = invite.guild if not guild or not isinstance(guild, discord.Guild): return inviter_str = "Unknown User" if invite.inviter and isinstance(invite.inviter, (discord.User, discord.Member)): inviter_str = invite.inviter.mention elif invite.inviter: inviter_str = f"User ({invite.inviter.id})" channel_str = "Unknown Channel" if invite.channel and isinstance(invite.channel, (discord.abc.GuildChannel)): if hasattr(invite.channel, 'mention'): channel_str = invite.channel.mention else: channel_str = f"`{invite.channel.name}`" elif invite.channel: channel_str = f"Channel ID: {invite.channel.id}" embed = discord.Embed( title="Invite Created", description=f"Invite `{invite.code}` to {channel_str} created by {inviter_str}.", color=discord.Color.teal(), timestamp=datetime.datetime.now(datetime.timezone.utc), ) embed.add_field(name="Max Uses", value=invite.max_uses or "Unlimited") embed.add_field( name="Max Age", value=f"{invite.max_age // 3600} hours" if invite.max_age else "Permanent", ) await self._send_log(guild, embed, "on_invite_create") async def setup(bot): await bot.add_cog(Logging(bot))