diff --git a/payphone/bot.py b/payphone/bot.py index 30ce2c4..bbede17 100644 --- a/payphone/bot.py +++ b/payphone/bot.py @@ -287,6 +287,7 @@ class Payphone(commands.AutoShardedBot): report: models.Report | None, expires_at: datetime | None, created_by_id: int | None, + reason: str | None = None, ) -> models.Ban: """Bans a user. @@ -295,7 +296,8 @@ class Payphone(commands.AutoShardedBot): report (Report | None): The report that caused the ban. expires_at (datetime | None): When the ban expires. created_by_id (int | None): The ID of the user who created the ban. - + reason (str | None): The reason for the ban. + Returns: (Ban): The ban. """ @@ -305,6 +307,7 @@ class Payphone(commands.AutoShardedBot): report=report, expires_at=expires_at, created_by_id=created_by_id, + reason=reason, ) await ban.save() @@ -456,7 +459,6 @@ def run(): sentry_sdk.init( constants.SENTRY_DSN, environment="development" if constants.IS_DEBUG else "production", - debug=constants.IS_DEBUG, server_name=f"Shard #{constants.SHARD_ID + 1}", attach_stacktrace=True, send_default_pii=True, diff --git a/payphone/cogs/admin/cog.py b/payphone/cogs/admin/cog.py index 4013a01..f307fb3 100644 --- a/payphone/cogs/admin/cog.py +++ b/payphone/cogs/admin/cog.py @@ -51,12 +51,19 @@ class Admin(base.Cog): @commands.command(aliases=["b"]) @checks.is_moderator() async def ban( - self, ctx: commands.Context, user: discord.User | int | None = None + self, + ctx: commands.Context, + user: discord.User | int | None = None, + duration: str | None = None, + *, + reason: str | None = None, ) -> None: """Bans the user from the bot. Options: - `[user]`: The user to ban or their ID. If none is provided, the author will be banned. + - `[duration]`: The duration of the ban (e.g 5d, 4h, 1M). If none is provided, the ban will be permanent. + - `[reason]`: The reason for the ban. Permissions: - Bot Moderator @@ -64,7 +71,21 @@ class Admin(base.Cog): user_id = user if isinstance(user, int) else user.id if user else ctx.author.id - await self.bot.ban(user_id, None, None, ctx.author.id) + if duration: + try: + duration = formatting.parse_timedelta(duration) + + except ValueError: + reason = f"{duration} {reason}" if reason else duration + duration = None + + await self.bot.ban( + user_id, + None, + datetime.now(timezone.utc) + duration if duration else None, + ctx.author.id, + reason, + ) await ctx.send(f"<@!{user_id}> was banned from Payphone.") @@ -149,17 +170,19 @@ class Admin(base.Cog): @commands.command() @checks.is_moderator() - async def bans(self, ctx: commands.Context, user: discord.User | int) -> None: + async def bans( + self, ctx: commands.Context, user: discord.User | int | None = None + ) -> None: """Shows all bans of a specific user. Options: - - ``: The user to show the bans of. + - `[user]`: The user to show the bans of. Permissions: - Bot Moderator """ - user_id = user if isinstance(user, int) else user.id + user_id = user if isinstance(user, int) else user.id if user else ctx.author.id bans = ( await models.Ban.filter(user_id=user_id) @@ -175,7 +198,7 @@ class Admin(base.Cog): [ ( discord.Embed( - color=constants.PRIMARY_COLOR, + color=constants.MODERATOR_COLOR, title=f"Ban #{ban.id}", description=formatting.trim(ban.report.message_content, 500), author=discord.EmbedAuthor( @@ -218,9 +241,14 @@ class Admin(base.Cog): ) if ban.report else discord.Embed( - color=constants.PRIMARY_COLOR, + color=constants.MODERATOR_COLOR, title=f"Ban #{ban.id}", - description=f"The user was banned manually by <@!{ban.created_by.id}> . It expires {f"" if ban.expires_at else "never"}.", + description=f"The user was banned manually by <@!{ban.created_by.id}> . {(f"It expires " if not ban.is_expired else f"It expired ") if ban.expires_at else "It doesn't expire"}.", + fields=[ + discord.EmbedField( + name="Reason", value=ban.reason or "No reason provided." + ) + ], footer=discord.EmbedFooter( text=f"Banned by {(await self.bot.get_or_fetch_user(ban.created_by.id)).display_name}" + ( diff --git a/payphone/core/formatting.py b/payphone/core/formatting.py index 732b3a7..cd34d26 100644 --- a/payphone/core/formatting.py +++ b/payphone/core/formatting.py @@ -201,7 +201,7 @@ def parse_timedelta(text: str) -> timedelta: ) matches = pattern.match(text.strip()) - if not matches: + if matches.span() == (0, 0): raise ValueError("Invalid time string format") years, months, weeks, days, hours, minutes, seconds = matches.groups() diff --git a/payphone/db/migrations/models/13_20250318130207_update.py b/payphone/db/migrations/models/13_20250318130207_update.py new file mode 100644 index 0000000..5275dc6 --- /dev/null +++ b/payphone/db/migrations/models/13_20250318130207_update.py @@ -0,0 +1,11 @@ +from tortoise import BaseDBAsyncClient + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + ALTER TABLE "ban" ADD "reason" TEXT;""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + ALTER TABLE "ban" DROP COLUMN "reason";""" diff --git a/payphone/db/models.py b/payphone/db/models.py index 748352d..f00876c 100644 --- a/payphone/db/models.py +++ b/payphone/db/models.py @@ -146,11 +146,20 @@ class Ban(models.Model): user_id = fields.BigIntField() report = fields.ForeignKeyField("models.Report", null=True) + reason = fields.TextField(null=True, default=None) created_at = fields.DatetimeField(auto_now_add=True) expires_at = fields.DatetimeField(null=True) created_by = fields.ForeignKeyField("models.User", related_name="created_bans") expired_by = fields.ForeignKeyField("models.User", related_name="expired_bans", null=True) + @property + def is_expired(self) -> bool: + """ + Whether the ban is expired. + """ + + return self.expires_at is not None and self.expires_at < datetime.now(timezone.utc) + class Meta: """ Meta information. diff --git a/uv.lock b/uv.lock index 3bf3dfb..542fb8e 100644 --- a/uv.lock +++ b/uv.lock @@ -413,7 +413,7 @@ wheels = [ [[package]] name = "payphone" -version = "0.58.0" +version = "0.59.0" source = { virtual = "." } dependencies = [ { name = "aerich" },