325 lines
12 KiB
Python
325 lines
12 KiB
Python
"""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))
|