quick commit
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 6m9s
CI/CD Pipeline / Security Scanning (push) Successful in 26s
CI/CD Pipeline / Tests (3.11) (push) Failing after 5m24s
CI/CD Pipeline / Tests (3.12) (push) Failing after 5m23s
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Deploy to Staging (push) Has been skipped
CI/CD Pipeline / Deploy to Production (push) Has been skipped
CI/CD Pipeline / Notification (push) Successful in 1s
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 6m9s
CI/CD Pipeline / Security Scanning (push) Successful in 26s
CI/CD Pipeline / Tests (3.11) (push) Failing after 5m24s
CI/CD Pipeline / Tests (3.12) (push) Failing after 5m23s
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Deploy to Staging (push) Has been skipped
CI/CD Pipeline / Deploy to Production (push) Has been skipped
CI/CD Pipeline / Notification (push) Successful in 1s
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
"""Moderation commands and automod features."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import discord
|
||||
@@ -10,36 +9,43 @@ from sqlalchemy import func, select
|
||||
|
||||
from guardden.bot import GuardDen
|
||||
from guardden.models import ModerationLog, Strike
|
||||
from guardden.utils import parse_duration
|
||||
from guardden.utils.ratelimit import RateLimitExceeded
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_duration(duration_str: str) -> timedelta | None:
|
||||
"""Parse a duration string like '1h', '30m', '7d' into a timedelta."""
|
||||
match = re.match(r"^(\d+)([smhdw])$", duration_str.lower())
|
||||
if not match:
|
||||
return None
|
||||
|
||||
amount = int(match.group(1))
|
||||
unit = match.group(2)
|
||||
|
||||
units = {
|
||||
"s": timedelta(seconds=amount),
|
||||
"m": timedelta(minutes=amount),
|
||||
"h": timedelta(hours=amount),
|
||||
"d": timedelta(days=amount),
|
||||
"w": timedelta(weeks=amount),
|
||||
}
|
||||
|
||||
return units.get(unit)
|
||||
|
||||
|
||||
class Moderation(commands.Cog):
|
||||
"""Moderation commands for server management."""
|
||||
|
||||
def __init__(self, bot: GuardDen) -> None:
|
||||
self.bot = bot
|
||||
|
||||
def cog_check(self, ctx: commands.Context) -> bool:
|
||||
if not ctx.guild:
|
||||
return False
|
||||
if not self.bot.is_owner_allowed(ctx.author.id):
|
||||
return False
|
||||
return True
|
||||
|
||||
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."
|
||||
)
|
||||
|
||||
async def _log_action(
|
||||
self,
|
||||
guild: discord.Guild,
|
||||
@@ -334,7 +340,15 @@ class Moderation(commands.Cog):
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
|
||||
await member.kick(reason=f"{ctx.author}: {reason}")
|
||||
try:
|
||||
await member.kick(reason=f"{ctx.author}: {reason}")
|
||||
except discord.Forbidden:
|
||||
await ctx.send("❌ I don't have permission to kick this member.")
|
||||
return
|
||||
except discord.HTTPException as e:
|
||||
await ctx.send(f"❌ Failed to kick member: {e}")
|
||||
return
|
||||
|
||||
await self._log_action(ctx.guild, member, ctx.author, "kick", reason)
|
||||
|
||||
embed = discord.Embed(
|
||||
@@ -346,7 +360,10 @@ class Moderation(commands.Cog):
|
||||
embed.add_field(name="Reason", value=reason, inline=False)
|
||||
embed.set_footer(text=f"Moderator: {ctx.author}")
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
try:
|
||||
await ctx.send(embed=embed)
|
||||
except discord.HTTPException:
|
||||
await ctx.send(f"✅ {member} has been kicked from the server.")
|
||||
|
||||
@commands.command(name="ban")
|
||||
@commands.has_permissions(ban_members=True)
|
||||
@@ -376,7 +393,15 @@ class Moderation(commands.Cog):
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
|
||||
await ctx.guild.ban(member, reason=f"{ctx.author}: {reason}", delete_message_days=0)
|
||||
try:
|
||||
await ctx.guild.ban(member, reason=f"{ctx.author}: {reason}", delete_message_days=0)
|
||||
except discord.Forbidden:
|
||||
await ctx.send("❌ I don't have permission to ban this member.")
|
||||
return
|
||||
except discord.HTTPException as e:
|
||||
await ctx.send(f"❌ Failed to ban member: {e}")
|
||||
return
|
||||
|
||||
await self._log_action(ctx.guild, member, ctx.author, "ban", reason)
|
||||
|
||||
embed = discord.Embed(
|
||||
@@ -388,7 +413,10 @@ class Moderation(commands.Cog):
|
||||
embed.add_field(name="Reason", value=reason, inline=False)
|
||||
embed.set_footer(text=f"Moderator: {ctx.author}")
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
try:
|
||||
await ctx.send(embed=embed)
|
||||
except discord.HTTPException:
|
||||
await ctx.send(f"✅ {member} has been banned from the server.")
|
||||
|
||||
@commands.command(name="unban")
|
||||
@commands.has_permissions(ban_members=True)
|
||||
|
||||
Reference in New Issue
Block a user