From 95489cc43c0f156456fe340941f022bfab6c4949 Mon Sep 17 00:00:00 2001 From: "airo.pi_" <47398145+AiroPi@users.noreply.github.com> Date: Sun, 17 Dec 2023 08:53:30 +0100 Subject: [PATCH 1/3] optimize before and after parameters Using the before and after parameters given by the API. Also, change parameters descriptions. Also add ranges for amount and lengths and split length in 2 parameters. --- .gitignore | 1 + requirements.txt | 1 + src/cogs/clear/__init__.py | 63 +++++++++++++++------------ src/cogs/clear/clear_transformers.py | 65 +++++++++------------------- src/cogs/clear/filters.py | 10 ----- 5 files changed, 58 insertions(+), 82 deletions(-) diff --git a/.gitignore b/.gitignore index 7f5365c..cbe69a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ data/ config.toml +draft/ # editors .vscode/ diff --git a/requirements.txt b/requirements.txt index 80e7e19..a9a0c45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ two048 # a personal library topggpy==2.0.0a0 lingua-language-detector aiohttp +python-dateutil diff --git a/src/cogs/clear/__init__.py b/src/cogs/clear/__init__.py index 900a7ef..a2ebe34 100644 --- a/src/cogs/clear/__init__.py +++ b/src/cogs/clear/__init__.py @@ -3,21 +3,21 @@ import asyncio import logging import time +from datetime import datetime from typing import TYPE_CHECKING, AsyncGenerator, Awaitable, Callable, Self, cast import discord from discord import app_commands, ui -from discord.app_commands import Transform, locale_str as __ +from discord.app_commands import Range, Transform, locale_str as __ from core import ExtendedCog, Menu, MessageDisplay, ResponseType, response_constructor from core.checkers import MaxConcurrency from core.errors import BadArgument, MaxConcurrencyReached, NonSpecificError, UnexpectedError from core.i18n import _ +from core.transformers import DateTransformer from core.utils import async_all from .clear_transformers import ( - AfterTransformer, - BeforeTransformer, HasTransformer, LengthTransformer, PinnedTransformer, @@ -25,7 +25,7 @@ RoleTransformer, UserTransformer, ) -from .filters import DateFilter, Filter, HasFilter, LengthFilter, PinnedFilter, RegexFilter, RoleFilter, UserFilter +from .filters import Filter, HasFilter, LengthFilter, PinnedFilter, RegexFilter, RoleFilter, UserFilter if TYPE_CHECKING: from discord import TextChannel, Thread, VoiceChannel @@ -55,21 +55,16 @@ def __init__(self, bot: MyBot): @app_commands.default_permissions(manage_messages=True) @app_commands.guild_only() @app_commands.describe( - amount=__("The amount of messages to delete."), - user=__("Delete only messages from the specified user."), - role=__("Delete only messages whose user has the specified role."), - pattern=__( - "Delete only messages that match the specified search (can be regex)." - ), # e.g. regex:hello will delete the message "hello world". - has=__( - "Delete only messages that contains the selected entry. #TODO" - ), # e.g. attachement:image will delete messages that has an image attached. - length=__("Delete only messages where length match the specified entry. (e.g. '<=100', '5', '>10') #TODO"), - before=__("Delete only messages sent before the specified message or date. (yyyy-mm-dd) #TODO"), - after=__("Delete only messages sent after the specified message or date. (yyyy-mm-dd) #TODO"), - pinned=__( - 'Include/exclude pinned messages in deletion, or deletes "only" pinned messages. (default to exclude)' - ), + amount=__("{{}} messages (max 250)"), + user=__("messages from the user {{}}"), + role=__("messages whose user has the role {{}}"), + pattern=__("messages that match {{}} (can be regex)"), + has=__("messages that has {{}}"), # e.g. attachement:image will delete messages that has an image attached. + max_length=__("messages longer or equal to {{}} (blank spaces included)"), + min_length=__("messages shorter or equal to {{}} (blank spaces included)"), + before=__("messages sent before {{}} (yyyy-mm-dd or message ID)"), + after=__("messages sent after {{}} (yyyy-mm-dd or message ID)"), + pinned=__("only pinned message or exclude them from the deletion. (default to exclude)"), ) @app_commands.rename( amount=__("amount"), @@ -77,7 +72,8 @@ def __init__(self, bot: MyBot): role=__("role"), pattern=__("search"), has=__("has"), - length=__("length"), + max_length=__("max_length"), + min_length=__("min_length"), before=__("before"), after=__("after"), pinned=__("pinned"), @@ -85,14 +81,15 @@ def __init__(self, bot: MyBot): async def clear( self, inter: discord.Interaction, - amount: int, + amount: Range[int, 1, 250], user: Transform[UserFilter, UserTransformer] | None = None, role: Transform[RoleFilter, RoleTransformer] | None = None, pattern: Transform[RegexFilter, RegexTransformer] | None = None, has: Transform[HasFilter, HasTransformer] | None = None, - length: Transform[LengthFilter, LengthTransformer] | None = None, - before: Transform[DateFilter, BeforeTransformer] | None = None, - after: Transform[DateFilter, AfterTransformer] | None = None, + max_length: Transform[LengthFilter, LengthTransformer("max")] | None = None, + min_length: Transform[LengthFilter, LengthTransformer("min")] | None = None, + before: Transform[datetime, DateTransformer] | None = None, + after: Transform[datetime, DateTransformer] | None = None, pinned: Transform[PinnedFilter, PinnedTransformer] = PinnedFilter.default(), ): await self.clear_max_concurrency.acquire(inter) @@ -106,10 +103,18 @@ async def clear( # Because of @guild_only, we can assume that the channel is a guild channel # Also, the channel should not be able to be a ForumChannel or StageChannel or CategoryChannel - available_filters: list[Filter | None] = [pinned, user, role, pattern, has, length, before, after] + available_filters: list[Filter | None] = [ + pinned, + user, + role, + pattern, + has, + max_length, + min_length, + ] active_filters: list[Filter] = [f for f in available_filters if f is not None] - job = ClearWorker(self.bot, inter, amount, active_filters) + job = ClearWorker(self.bot, inter, amount, active_filters, before, after) await job.start() await self.clear_max_concurrency.release(inter) @@ -126,12 +131,16 @@ def __init__( inter: discord.Interaction, amount: int, filters: list[Filter], + before: datetime | None, + after: datetime | None, ): self.deleted_messages: int = 0 self.analyzed_messages: int = 0 self.deletion_planned: int = 0 self.deletion_goal: int = amount self.filters = filters + self.before = before + self.after = after self.inter = inter self.bot = bot self.channel = cast("AllowPurgeChannel", inter.channel) @@ -241,7 +250,7 @@ async def _bulk_delete_strategy(messages: list[discord.Message]) -> None: async def filtered_history(self) -> AsyncGenerator[discord.Message, None]: limit = self.deletion_goal if not any(self.filters) else None - async for msg in self.channel.history(limit=limit): + async for msg in self.channel.history(limit=limit, before=self.before, after=self.after): # if not all the filters tests are compliant, continue self.analyzed_messages += 1 if not await async_all(await filter.test(msg) for filter in self.filters): diff --git a/src/cogs/clear/clear_transformers.py b/src/cogs/clear/clear_transformers.py index 27058b5..10a1e77 100644 --- a/src/cogs/clear/clear_transformers.py +++ b/src/cogs/clear/clear_transformers.py @@ -1,17 +1,16 @@ from __future__ import annotations -import re -from operator import eq, ge, gt, le, lt -from typing import Callable +from operator import ge, le +from typing import Literal from discord import AppCommandOptionType, Interaction from discord.app_commands import Choice, Transformer, locale_str as __ from core.errors import NonSpecificError -from core.transformers import DateTransformer, SimpleTransformer +from core.transformers import SimpleTransformer from .enums import Pinned -from .filters import DateFilter, Has, HasFilter, LengthFilter, PinnedFilter, RegexFilter, RoleFilter, UserFilter +from .filters import Has, HasFilter, LengthFilter, PinnedFilter, RegexFilter, RoleFilter, UserFilter RegexTransformer = SimpleTransformer(RegexFilter.from_string) UserTransformer = SimpleTransformer(UserFilter.from_user, AppCommandOptionType.user) @@ -61,52 +60,28 @@ async def transform(self, inter: Interaction, value: int) -> HasFilter: class LengthTransformer(Transformer): - identifiers: dict[str, Callable[[int, int], bool]] = { - "<": lt, - "<=": le, - ">": gt, - ">=": ge, - "=": eq, - "": eq, - } - regex = re.compile(r"^(<|>|<=|>=|=|)(\d+)$") + def __init__(self, mode: Literal["min", "max"]): + self.mode = mode @property def type(self) -> AppCommandOptionType: - return AppCommandOptionType.string - - async def transform(self, inter: Interaction, value: str) -> LengthFilter: - del inter # unused - - result = self.regex.match(value) - - if not result: - raise NonSpecificError("Invalid length filter") - - length = int(result.group(2)) - - if not 0 <= length <= 4000: - raise NonSpecificError("A length filter must be between 0 and 4000 characters") - - test = self.identifiers[result.group(1)] - return LengthFilter(test, length) - + return AppCommandOptionType.integer -class BeforeTransformer(Transformer): @property - def type(self) -> AppCommandOptionType: - return AppCommandOptionType.string + def min_value(self) -> int: + return 1 - async def transform(self, inter: Interaction, value: str) -> DateFilter: - inner_transformer = DateTransformer() - return DateFilter(lt, await inner_transformer.transform(inter, value)) + @property + def max_value(self) -> int: + return 4000 + async def transform(self, inter: Interaction, value: int) -> LengthFilter: + del inter # unused -class AfterTransformer(Transformer): - @property - def type(self) -> AppCommandOptionType: - return AppCommandOptionType.string + if not 0 <= value <= 4000: + raise NonSpecificError("A length filter must be between 0 and 4000 characters") - async def transform(self, inter: Interaction, value: str) -> DateFilter: - inner_transformer = DateTransformer() - return DateFilter(gt, await inner_transformer.transform(inter, value)) + if self.mode == "min": + return LengthFilter(ge, value) + else: + return LengthFilter(le, value) diff --git a/src/cogs/clear/filters.py b/src/cogs/clear/filters.py index 52be38a..335f70a 100644 --- a/src/cogs/clear/filters.py +++ b/src/cogs/clear/filters.py @@ -1,6 +1,5 @@ from __future__ import annotations -import datetime import re from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Callable, cast @@ -147,12 +146,3 @@ def __init__(self, length_test: Callable[[int, int], bool], length: int): async def test(self, message: discord.Message) -> bool: return self.length_test(len(message.content), self.length) - - -class DateFilter(Filter): - def __init__(self, date_test: Callable[[datetime.datetime, datetime.datetime], bool], date: datetime.datetime): - self.date_test = date_test - self.date = date - - async def test(self, message: discord.Message) -> bool: - return self.date_test(message.created_at, self.date) From ea42abef3bf5d4e9a0153aa439b71a168d2de3b3 Mon Sep 17 00:00:00 2001 From: "airo.pi_" <47398145+AiroPi@users.noreply.github.com> Date: Sun, 17 Dec 2023 09:02:56 +0100 Subject: [PATCH 2/3] Update parameters descriptions --- src/cogs/clear/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cogs/clear/__init__.py b/src/cogs/clear/__init__.py index a2ebe34..f2a13eb 100644 --- a/src/cogs/clear/__init__.py +++ b/src/cogs/clear/__init__.py @@ -58,9 +58,9 @@ def __init__(self, bot: MyBot): amount=__("{{}} messages (max 250)"), user=__("messages from the user {{}}"), role=__("messages whose user has the role {{}}"), - pattern=__("messages that match {{}} (can be regex)"), + pattern=__("messages that match {{}} (regex, multiline, case sensitive, not anchored)"), has=__("messages that has {{}}"), # e.g. attachement:image will delete messages that has an image attached. - max_length=__("messages longer or equal to {{}} (blank spaces included)"), + max_length=__("messages longer or equal to {{}} (blank spaces included) (empty messages included)"), min_length=__("messages shorter or equal to {{}} (blank spaces included)"), before=__("messages sent before {{}} (yyyy-mm-dd or message ID)"), after=__("messages sent after {{}} (yyyy-mm-dd or message ID)"), From 17e6b5a20e60e89127034cb2dc6e6adf88a05319 Mon Sep 17 00:00:00 2001 From: "airo.pi_" <47398145+AiroPi@users.noreply.github.com> Date: Sun, 17 Dec 2023 09:28:13 +0100 Subject: [PATCH 3/3] Add bot required permissions --- src/cogs/clear/__init__.py | 14 ++++++-------- src/core/checkers/__init__.py | 1 + src/core/error_handler.py | 13 ++++++------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/cogs/clear/__init__.py b/src/cogs/clear/__init__.py index f2a13eb..fe4feec 100644 --- a/src/cogs/clear/__init__.py +++ b/src/cogs/clear/__init__.py @@ -10,8 +10,7 @@ from discord import app_commands, ui from discord.app_commands import Range, Transform, locale_str as __ -from core import ExtendedCog, Menu, MessageDisplay, ResponseType, response_constructor -from core.checkers import MaxConcurrency +from core import ExtendedCog, Menu, MessageDisplay, ResponseType, checkers, response_constructor from core.errors import BadArgument, MaxConcurrencyReached, NonSpecificError, UnexpectedError from core.i18n import _ from core.transformers import DateTransformer @@ -46,11 +45,13 @@ class Clear(ExtendedCog): def __init__(self, bot: MyBot): super().__init__(bot) - self.clear_max_concurrency = MaxConcurrency(1, key=channel_bucket, wait=False) + self.clear_max_concurrency = checkers.MaxConcurrency(1, key=channel_bucket, wait=False) + @checkers.app.bot_required_permissions( + manage_messages=True, read_message_history=True, read_messages=True, connect=True + ) @app_commands.command( - description=__("Delete multiple messages with some filters."), - extras={"beta": True}, + description=__("Delete multiple messages with filters."), ) @app_commands.default_permissions(manage_messages=True) @app_commands.guild_only() @@ -100,9 +101,6 @@ async def clear( if not 0 < amount < 251: raise BadArgument(_("You must supply a number between 1 and 250. (0 < {amount} < 251)", amount=amount)) - # Because of @guild_only, we can assume that the channel is a guild channel - # Also, the channel should not be able to be a ForumChannel or StageChannel or CategoryChannel - available_filters: list[Filter | None] = [ pinned, user, diff --git a/src/core/checkers/__init__.py b/src/core/checkers/__init__.py index 50307ab..49f488c 100644 --- a/src/core/checkers/__init__.py +++ b/src/core/checkers/__init__.py @@ -1 +1,2 @@ +from . import app as app from .max_concurrency import MaxConcurrency as MaxConcurrency diff --git a/src/core/error_handler.py b/src/core/error_handler.py index e7932b2..7128d70 100644 --- a/src/core/error_handler.py +++ b/src/core/error_handler.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Literal, TypeVar from discord import ButtonStyle, Interaction, ui -from discord.app_commands.errors import AppCommandError, CheckFailure, CommandNotFound, NoPrivateMessage +from discord.app_commands.errors import AppCommandError, CommandNotFound, NoPrivateMessage from discord.ext import commands from . import ResponseType, response_constructor @@ -14,7 +14,6 @@ BotMissingPermissions, BotUserNotPresent, MaxConcurrencyReached, - MiscCheckFailure, MiscNoPrivateMessage, NonSpecificError, ) @@ -63,21 +62,21 @@ async def handle(self, ctx: Interaction | MiscCommandContext[MyBot], error: Exce ctx, _("This command is already executed the max amount of times. (Max: {error.rate})", error=error), ) - case CheckFailure() | MiscCheckFailure(): - return await self.send_error(ctx, _("This command needs some conditions you don't meet.")) - case BadArgument(): # Interactions only - # TODO(airo.pi_): improve this ? - return await self.send_error(ctx, _("You provided a bad argument.")) case BotMissingPermissions(): return await self.send_error( ctx, _("The bot is missing some permissions.\n`{}`", "`, `".join(error.missing_perms)) ) + case BadArgument(): # Interactions only + # TODO(airo.pi_): improve this ? + return await self.send_error(ctx, _("You provided a bad argument.")) case BotUserNotPresent(): return await self.send_error( ctx, _("It looks like the bot has been added incorrectly. Please ask an admin to re-add the bot.") ) case MiscNoPrivateMessage() | NoPrivateMessage(): return await self.send_error(ctx, _("This command cannot be used in DMs.")) + # case CheckFailure() | MiscCheckFailure(): + # return await self.send_error(ctx, _("This command needs some conditions you don't meet.")) case _: await self.send_error( ctx, _("An unhandled error happened.\nPlease ask on the support server!", error=error)