"""Admin commands for bot configuration.""" import logging from typing import Literal import discord from discord.ext import commands from guardden.bot import GuardDen from guardden.utils.ratelimit import RateLimitExceeded logger = logging.getLogger(__name__) class Admin(commands.Cog): """Administrative commands for bot configuration.""" def __init__(self, bot: GuardDen) -> None: self.bot = bot def cog_check(self, ctx: commands.Context) -> bool: """Ensure only administrators can use these commands.""" if not ctx.guild: return False if not self.bot.is_owner_allowed(ctx.author.id): return False return ctx.author.guild_permissions.administrator async def cog_before_invoke(self, ctx: commands.Context) -> None: if not ctx.command: return result = self.bot.rate_limiter.acquire_command( ctx.command.qualified_name, user_id=ctx.author.id, guild_id=ctx.guild.id if ctx.guild else None, channel_id=ctx.channel.id, ) if result.is_limited: raise RateLimitExceeded(result.reset_after) async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None: if isinstance(error, RateLimitExceeded): await ctx.send( f"You're being rate limited. Try again in {error.retry_after:.1f} seconds." ) @commands.group(name="config", invoke_without_command=True) @commands.guild_only() async def config(self, ctx: commands.Context) -> None: """View or modify bot configuration.""" config = await self.bot.guild_config.get_config(ctx.guild.id) if not config: await ctx.send("No configuration found. Run a config command to initialize.") return embed = discord.Embed( title=f"Configuration for {ctx.guild.name}", color=discord.Color.blue(), ) # General settings embed.add_field(name="Prefix", value=f"`{config.prefix}`", inline=True) embed.add_field(name="Locale", value=config.locale, inline=True) embed.add_field(name="\u200b", value="\u200b", inline=True) # Channels log_ch = ctx.guild.get_channel(config.log_channel_id) if config.log_channel_id else None mod_log_ch = ( ctx.guild.get_channel(config.mod_log_channel_id) if config.mod_log_channel_id else None ) welcome_ch = ( ctx.guild.get_channel(config.welcome_channel_id) if config.welcome_channel_id else None ) embed.add_field( name="Log Channel", value=log_ch.mention if log_ch else "Not set", inline=True ) embed.add_field( name="Mod Log Channel", value=mod_log_ch.mention if mod_log_ch else "Not set", inline=True, ) embed.add_field( name="Welcome Channel", value=welcome_ch.mention if welcome_ch else "Not set", inline=True, ) # Features features = [] if config.automod_enabled: features.append("AutoMod") if config.anti_spam_enabled: features.append("Anti-Spam") if config.link_filter_enabled: features.append("Link Filter") if config.ai_moderation_enabled: features.append("AI Moderation") if config.verification_enabled: features.append("Verification") embed.add_field( name="Enabled Features", value=", ".join(features) if features else "None", inline=False, ) # Notification settings embed.add_field( name="In-Channel Warnings", value="✅ Enabled" if config.send_in_channel_warnings else "❌ Disabled", inline=True, ) await ctx.send(embed=embed) @config.command(name="prefix") @commands.guild_only() async def config_prefix(self, ctx: commands.Context, prefix: str) -> None: """Set the command prefix for this server.""" if not prefix or not prefix.strip(): await ctx.send("Prefix cannot be empty or whitespace only.") return if len(prefix) > 10: await ctx.send("Prefix must be 10 characters or less.") return await self.bot.guild_config.update_settings(ctx.guild.id, prefix=prefix) await ctx.send(f"Command prefix set to `{prefix}`") @config.command(name="logchannel") @commands.guild_only() async def config_log_channel( self, ctx: commands.Context, channel: discord.TextChannel | None = None ) -> None: """Set the channel for general event logs.""" channel_id = channel.id if channel else None await self.bot.guild_config.update_settings(ctx.guild.id, log_channel_id=channel_id) if channel: await ctx.send(f"Log channel set to {channel.mention}") else: await ctx.send("Log channel has been disabled.") @config.command(name="modlogchannel") @commands.guild_only() async def config_mod_log_channel( self, ctx: commands.Context, channel: discord.TextChannel | None = None ) -> None: """Set the channel for moderation action logs.""" channel_id = channel.id if channel else None await self.bot.guild_config.update_settings(ctx.guild.id, mod_log_channel_id=channel_id) if channel: await ctx.send(f"Moderation log channel set to {channel.mention}") else: await ctx.send("Moderation log channel has been disabled.") @config.command(name="welcomechannel") @commands.guild_only() async def config_welcome_channel( self, ctx: commands.Context, channel: discord.TextChannel | None = None ) -> None: """Set the welcome channel for new members.""" channel_id = channel.id if channel else None await self.bot.guild_config.update_settings(ctx.guild.id, welcome_channel_id=channel_id) if channel: await ctx.send(f"Welcome channel set to {channel.mention}") else: await ctx.send("Welcome channel has been disabled.") @config.command(name="muterole") @commands.guild_only() async def config_mute_role( self, ctx: commands.Context, role: discord.Role | None = None ) -> None: """Set the role to assign when muting members.""" role_id = role.id if role else None await self.bot.guild_config.update_settings(ctx.guild.id, mute_role_id=role_id) if role: await ctx.send(f"Mute role set to {role.mention}") else: await ctx.send("Mute role has been cleared.") @config.command(name="automod") @commands.guild_only() async def config_automod(self, ctx: commands.Context, enabled: bool) -> None: """Enable or disable automod features.""" await self.bot.guild_config.update_settings(ctx.guild.id, automod_enabled=enabled) status = "enabled" if enabled else "disabled" await ctx.send(f"AutoMod has been {status}.") @config.command(name="antispam") @commands.guild_only() async def config_antispam(self, ctx: commands.Context, enabled: bool) -> None: """Enable or disable anti-spam protection.""" await self.bot.guild_config.update_settings(ctx.guild.id, anti_spam_enabled=enabled) status = "enabled" if enabled else "disabled" await ctx.send(f"Anti-spam has been {status}.") @config.command(name="linkfilter") @commands.guild_only() async def config_linkfilter(self, ctx: commands.Context, enabled: bool) -> None: """Enable or disable link filtering.""" await self.bot.guild_config.update_settings(ctx.guild.id, link_filter_enabled=enabled) status = "enabled" if enabled else "disabled" await ctx.send(f"Link filter has been {status}.") @commands.group(name="bannedwords", aliases=["bw"], invoke_without_command=True) @commands.guild_only() async def banned_words(self, ctx: commands.Context) -> None: """Manage banned words list.""" words = await self.bot.guild_config.get_banned_words(ctx.guild.id) if not words: await ctx.send("No banned words configured.") return embed = discord.Embed( title="Banned Words", color=discord.Color.red(), ) for word in words[:25]: # Discord embed limit word_type = "Regex" if word.is_regex else "Text" embed.add_field( name=f"#{word.id}: {word.pattern[:30]}", value=f"Type: {word_type} | Action: {word.action}", inline=True, ) if len(words) > 25: embed.set_footer(text=f"Showing 25 of {len(words)} banned words") await ctx.send(embed=embed) @banned_words.command(name="add") @commands.guild_only() async def banned_words_add( self, ctx: commands.Context, pattern: str, action: Literal["delete", "warn", "strike"] = "delete", is_regex: bool = False, ) -> None: """Add a banned word or pattern.""" word = await self.bot.guild_config.add_banned_word( guild_id=ctx.guild.id, pattern=pattern, added_by=ctx.author.id, is_regex=is_regex, action=action, ) word_type = "regex pattern" if is_regex else "word" await ctx.send(f"Added banned {word_type}: `{pattern}` (ID: {word.id}, Action: {action})") @banned_words.command(name="remove", aliases=["delete"]) @commands.guild_only() async def banned_words_remove(self, ctx: commands.Context, word_id: int) -> None: """Remove a banned word by ID.""" success = await self.bot.guild_config.remove_banned_word(ctx.guild.id, word_id) if success: await ctx.send(f"Removed banned word #{word_id}") else: await ctx.send(f"Banned word #{word_id} not found.") @commands.command(name="channelwarnings") @commands.guild_only() async def channel_warnings(self, ctx: commands.Context, enabled: bool) -> None: """Enable or disable PUBLIC in-channel warnings when DMs fail. WARNING: In-channel messages are PUBLIC and visible to all users in the channel. They are NOT private due to Discord API limitations. When enabled, if a user has DMs disabled, moderation warnings will be sent as temporary PUBLIC messages in the channel (auto-deleted after 10 seconds). Args: enabled: True to enable PUBLIC warnings, False to disable (default: False) """ await self.bot.guild_config.update_settings(ctx.guild.id, send_in_channel_warnings=enabled) status = "enabled" if enabled else "disabled" embed = discord.Embed( title="In-Channel Warnings Updated", description=f"In-channel warnings are now **{status}**.", color=discord.Color.green() if enabled else discord.Color.orange(), ) if enabled: embed.add_field( name="⚠️ Privacy Warning", value="**Messages are PUBLIC and visible to ALL users in the channel.**\n" "When a user has DMs disabled, moderation warnings will be sent " "as temporary PUBLIC messages in the channel (auto-deleted after 10 seconds).", inline=False, ) else: embed.add_field( name="✅ Privacy Protected", value="When users have DMs disabled, they will not receive any notification. " "This protects user privacy and prevents public embarrassment.", inline=False, ) await ctx.send(embed=embed) @commands.command(name="sync") @commands.is_owner() async def sync_commands(self, ctx: commands.Context) -> None: """Sync slash commands (bot owner only).""" await self.bot.tree.sync() await ctx.send("Slash commands synced.") async def setup(bot: GuardDen) -> None: """Load the Admin cog.""" await bot.add_cog(Admin(bot))