diff options
| -rw-r--r-- | bot/constants.py | 1 | ||||
| -rw-r--r-- | bot/exts/filtering/_filter_lists/antispam.py | 14 | ||||
| -rw-r--r-- | bot/exts/filtering/_ui/ui.py | 40 | ||||
| -rw-r--r-- | bot/exts/filtering/filtering.py | 36 | ||||
| -rw-r--r-- | config-default.yml | 1 |
5 files changed, 74 insertions, 18 deletions
diff --git a/bot/constants.py b/bot/constants.py index 8a2571a98..1d6ab8e7e 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -455,7 +455,6 @@ class Webhooks(metaclass=YAMLGetter): duck_pond: int incidents: int incidents_archive: int - filters: int class Roles(metaclass=YAMLGetter): diff --git a/bot/exts/filtering/_filter_lists/antispam.py b/bot/exts/filtering/_filter_lists/antispam.py index 41eda9878..e549404c4 100644 --- a/bot/exts/filtering/_filter_lists/antispam.py +++ b/bot/exts/filtering/_filter_lists/antispam.py @@ -10,17 +10,15 @@ from operator import add, or_ import arrow from botcore.utils import scheduling from botcore.utils.logging import get_logger -from discord import HTTPException, Member +from discord import Member -import bot -from bot.constants import Webhooks from bot.exts.filtering._filter_context import FilterContext from bot.exts.filtering._filter_lists.filter_list import ListType, SubscribingAtomicList, UniquesListBase from bot.exts.filtering._filters.antispam import antispam_filter_types from bot.exts.filtering._filters.filter import Filter, UniqueFilter from bot.exts.filtering._settings import ActionSettings from bot.exts.filtering._settings_types.actions.infraction_and_notification import Infraction -from bot.exts.filtering._ui.ui import build_mod_alert +from bot.exts.filtering._ui.ui import AlertView, build_mod_alert if typing.TYPE_CHECKING: from bot.exts.filtering.filtering import Filtering @@ -147,9 +145,9 @@ class DeletionContext: """Post the mod alert.""" if not self.contexts or not self.rules: return - try: - webhook = await bot.instance.fetch_webhook(Webhooks.filters) - except HTTPException: + + webhook = antispam_list.filtering_cog.webhook + if not webhook: return ctx, *other_contexts = self.contexts @@ -182,4 +180,4 @@ class DeletionContext: embed.set_footer( text="The list of actions taken includes actions from additional contexts after deletion began." ) - await webhook.send(username="Anti-Spam", content=ctx.alert_content, embeds=[embed]) + await webhook.send(username="Anti-Spam", content=ctx.alert_content, embeds=[embed], view=AlertView(new_ctx)) diff --git a/bot/exts/filtering/_ui/ui.py b/bot/exts/filtering/_ui/ui.py index ec549725c..e71bab0d1 100644 --- a/bot/exts/filtering/_ui/ui.py +++ b/bot/exts/filtering/_ui/ui.py @@ -20,6 +20,7 @@ import bot from bot.constants import Colours from bot.exts.filtering._filter_context import FilterContext from bot.exts.filtering._filter_lists import FilterList +from bot.exts.filtering._utils import FakeContext from bot.utils.messages import format_channel, format_user, upload_log log = get_logger(__name__) @@ -40,6 +41,8 @@ MAX_MODAL_TITLE_LENGTH = 45 # Max number of items in a select MAX_SELECT_ITEMS = 25 MAX_EMBED_DESCRIPTION = 4080 +# Number of seconds before timeout of the alert view +ALERT_VIEW_TIMEOUT = 3600 SETTINGS_DELIMITER = re.compile(r"\s+(?=\S+=\S+)") SINGLE_SETTING_PATTERN = re.compile(r"[\w/]+=.+") @@ -502,3 +505,40 @@ class DeleteConfirmationView(discord.ui.View): async def cancel(self, interaction: Interaction, button: discord.ui.Button) -> None: """Cancel the filter list deletion.""" await interaction.response.edit_message(content="🚫 Operation canceled.", view=None) + + +class AlertView(discord.ui.View): + """A view providing info about the offending user.""" + + def __init__(self, ctx: FilterContext): + super().__init__(timeout=ALERT_VIEW_TIMEOUT) + self.ctx = ctx + + @discord.ui.button(label="ID") + async def user_id(self, interaction: Interaction, button: discord.ui.Button) -> None: + """Reply with the ID of the offending user.""" + await interaction.response.send_message(self.ctx.author.id, ephemeral=True) + + @discord.ui.button(emoji="👤") + async def user_info(self, interaction: Interaction, button: discord.ui.Button) -> None: + """Send the info embed of the offending user.""" + command = bot.instance.get_command("user") + if not command: + await interaction.response.send_message("The command `user` is not loaded.", ephemeral=True) + return + + await interaction.response.defer() + fake_ctx = FakeContext(interaction.channel, command, author=interaction.user) + await command(fake_ctx, self.ctx.author) + + @discord.ui.button(emoji="⚠") + async def user_infractions(self, interaction: Interaction, button: discord.ui.Button) -> None: + """Send the infractions embed of the offending user.""" + command = bot.instance.get_command("infraction search") + if not command: + await interaction.response.send_message("The command `infraction search` is not loaded.", ephemeral=True) + return + + await interaction.response.defer() + fake_ctx = FakeContext(interaction.channel, command, author=interaction.user) + await command(fake_ctx, self.ctx.author) diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py index 673b5487c..2e433aff6 100644 --- a/bot/exts/filtering/filtering.py +++ b/bot/exts/filtering/filtering.py @@ -23,7 +23,8 @@ import bot import bot.exts.filtering._ui.filter as filters_ui from bot import constants from bot.bot import Bot -from bot.constants import Channels, MODERATION_ROLES, Roles, Webhooks +from bot.constants import Channels, Guild, MODERATION_ROLES, Roles +from bot.exts.backend.branding._repository import HEADERS, PARAMS from bot.exts.filtering._filter_context import Event, FilterContext from bot.exts.filtering._filter_lists import FilterList, ListType, filter_list_types, list_type_converter from bot.exts.filtering._filter_lists.filter_list import AtomicList @@ -36,7 +37,7 @@ from bot.exts.filtering._ui.filter import ( from bot.exts.filtering._ui.filter_list import FilterListAddView, FilterListEditView, settings_converter from bot.exts.filtering._ui.search import SearchEditView, search_criteria_converter from bot.exts.filtering._ui.ui import ( - ArgumentCompletionView, DeleteConfirmationView, build_mod_alert, format_response_error + AlertView, ArgumentCompletionView, DeleteConfirmationView, build_mod_alert, format_response_error ) from bot.exts.filtering._utils import past_tense, repr_equals, starting_value, to_serializable from bot.exts.moderation.infraction.infractions import COMP_BAN_DURATION, COMP_BAN_REASON @@ -48,6 +49,7 @@ from bot.utils.message_cache import MessageCache log = get_logger(__name__) +WEBHOOK_ICON_URL = r"https://github.com/python-discord/branding/raw/main/icons/filter/filter_pfp.png" CACHE_SIZE = 100 HOURS_BETWEEN_NICKNAME_ALERTS = 1 OFFENSIVE_MSG_DELETE_TIME = datetime.timedelta(days=7) @@ -70,7 +72,7 @@ class Filtering(Cog): self.filter_lists: dict[str, FilterList] = {} self._subscriptions: defaultdict[Event, list[FilterList]] = defaultdict(list) self.delete_scheduler = scheduling.Scheduler(self.__class__.__name__) - self.webhook = None + self.webhook: discord.Webhook = None self.loaded_settings = {} self.loaded_filters = {} @@ -93,10 +95,8 @@ class Filtering(Cog): if not example_list and loaded_list: example_list = loaded_list - try: - self.webhook = await self.bot.fetch_webhook(Webhooks.filters) - except HTTPException: - log.error(f"Failed to fetch filters webhook with ID `{Webhooks.filters}`.") + # The webhook must be generated by the bot to send messages with components through it. + self.webhook = await self._generate_webhook() self.collect_loaded_types(example_list) await self.schedule_offending_messages_deletion() @@ -861,6 +861,24 @@ class Filtering(Cog): self.filter_lists[list_name] = filter_list_types[list_name](self) return self.filter_lists[list_name].add_list(list_data) + async def _generate_webhook(self) -> discord.Webhook | None: + """Generate a webhook with the filtering avatar.""" + # Download the filtering avatar from the branding repository. + webhook_icon = None + async with self.bot.http_session.get(WEBHOOK_ICON_URL, params=PARAMS, headers=HEADERS) as response: + if response.status == 200: + log.debug("Successfully fetched filtering webhook icon, reading payload.") + webhook_icon = await response.read() + else: + log.warning(f"Failed to fetch filtering webhook icon due to status: {response.status}") + + alerts_channel = self.bot.get_guild(Guild.id).get_channel(Channels.mod_alerts) + try: + return await alerts_channel.create_webhook(name="Filtering System", avatar=webhook_icon) + except HTTPException: + log.error("Failed to create filters webhook.") + return None + async def _resolve_action( self, ctx: FilterContext ) -> tuple[ActionSettings | None, dict[FilterList, list[str]], dict[AtomicList, list[Filter]]]: @@ -895,7 +913,9 @@ class Filtering(Cog): name = f"{ctx.event.name.replace('_', ' ').title()} Filter" embed = await build_mod_alert(ctx, triggered_filters) # There shouldn't be more than 10, but if there are it's not very useful to send them all. - await self.webhook.send(username=name, content=ctx.alert_content, embeds=[embed, *ctx.alert_embeds][:10]) + await self.webhook.send( + username=name, content=ctx.alert_content, embeds=[embed, *ctx.alert_embeds][:10], view=AlertView(ctx) + ) async def _recently_alerted_name(self, member: discord.Member) -> bool: """When it hasn't been `HOURS_BETWEEN_NICKNAME_ALERTS` since last alert, return False, otherwise True.""" diff --git a/config-default.yml b/config-default.yml index 4407177d9..f0e217d6c 100644 --- a/config-default.yml +++ b/config-default.yml @@ -324,7 +324,6 @@ guild: incidents: 816650601844572212 incidents_archive: 720671599790915702 python_news: &PYNEWS_WEBHOOK 704381182279942324 - filters: 926442964463521843 keys: |