diff options
| -rw-r--r-- | bot/cogs/watchchannels/bigbrother.py | 22 | ||||
| -rw-r--r-- | bot/cogs/watchchannels/talentpool.py | 11 | ||||
| -rw-r--r-- | bot/cogs/watchchannels/watchchannel.py | 173 | ||||
| -rw-r--r-- | bot/utils/messages.py | 2 |
4 files changed, 99 insertions, 109 deletions
diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index 1721fefb9..dc5e76f55 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -14,7 +14,7 @@ log = logging.getLogger(__name__) class BigBrother(WatchChannel): - """User monitoring to assist with moderation""" + """Monitors users by relaying their messages to a watch channel to assist with moderation.""" def __init__(self, bot) -> None: super().__init__( @@ -29,7 +29,7 @@ class BigBrother(WatchChannel): @group(name='bigbrother', aliases=('bb',), invoke_without_command=True) @with_role(Roles.owner, Roles.admin, Roles.moderator) async def bigbrother_group(self, ctx: Context) -> None: - """Monitors users by relaying their messages to the BigBrother watch channel""" + """Monitors users by relaying their messages to the BigBrother watch channel.""" await ctx.invoke(self.bot.get_command("help"), "bigbrother") @bigbrother_group.command(name='watched', aliases=('all', 'list')) @@ -54,7 +54,7 @@ class BigBrother(WatchChannel): """ if user.bot: e = Embed( - description=f":x: **I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.**", + description=f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.", color=Color.red() ) await ctx.send(embed=e) @@ -66,7 +66,7 @@ class BigBrother(WatchChannel): if user.id in self.watched_users: e = Embed( - description=":x: **The specified user is already being watched**", + description=":x: The specified user is already being watched.", color=Color.red() ) await ctx.send(embed=e) @@ -78,7 +78,7 @@ class BigBrother(WatchChannel): if response is not None: self.watched_users[user.id] = response e = Embed( - description=f":white_check_mark: **Messages sent by {user} will now be relayed to BigBrother**", + description=f":white_check_mark: Messages sent by {user} will now be relayed to BigBrother.", color=Color.green() ) await ctx.send(embed=e) @@ -97,22 +97,24 @@ class BigBrother(WatchChannel): ) if active_watches: [infraction] = active_watches + await self.bot.api_client.patch( f"{self.api_endpoint}/{infraction['id']}", json={'active': False} ) - await post_infraction( - ctx, user, type='watch', reason=f"Unwatched: {reason}", hidden=True, active=False - ) + + await post_infraction(ctx, user, type='watch', reason=f"Unwatched: {reason}", hidden=True, active=False) + e = Embed( - description=f":white_check_mark: **Messages sent by {user} will no longer be relayed**", + description=f":white_check_mark: Messages sent by {user} will no longer be relayed.", color=Color.green() ) await ctx.send(embed=e) + self._remove_user(user.id) else: e = Embed( - description=":x: **The specified user is currently not being watched**", + description=":x: The specified user is currently not being watched.", color=Color.red() ) await ctx.send(embed=e) diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index e267d4594..5e515fe2e 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -17,7 +17,8 @@ STAFF_ROLES = Roles.owner, Roles.admin, Roles.moderator, Roles.helpers # <- I class TalentPool(WatchChannel): - """A TalentPool for helper nominees""" + """Relays messages of helper candidates to the talent-pool channel to observe them.""" + def __init__(self, bot) -> None: super().__init__( bot, @@ -109,8 +110,8 @@ class TalentPool(WatchChannel): ) await ctx.send(embed=e) return - - resp.raise_for_status() + else: + resp.raise_for_status() self.watched_users[user.id] = response_data e = Embed( @@ -122,7 +123,7 @@ class TalentPool(WatchChannel): @nomination_group.command(name='history', aliases=('info', 'search')) @with_role(Roles.owner, Roles.admin, Roles.moderator) async def history_command(self, ctx: Context, user: Union[User, proxy_user]) -> None: - """Shows the specified user's nomination history""" + """Shows the specified user's nomination history.""" result = await self.bot.api_client.get( self.api_endpoint, params={ @@ -233,7 +234,7 @@ class TalentPool(WatchChannel): await ctx.send(embed=e) def _nomination_to_string(self, nomination_object: dict) -> str: - """Creates a string representation of a nomination""" + """Creates a string representation of a nomination.""" guild = self.bot.get_guild(Guild.id) actor_id = nomination_object["actor"] diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index 566f7d52a..8f0bc765d 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -2,6 +2,7 @@ import asyncio import datetime import logging import re +import textwrap from abc import ABC, abstractmethod from collections import defaultdict, deque from typing import Optional @@ -11,7 +12,8 @@ import discord from discord import Color, Embed, Message, Object, errors from discord.ext.commands import BadArgument, Bot, Context -from bot.constants import BigBrother as BigBrotherConfig, Guild as GuildConfig +from bot.cogs.modlog import ModLog +from bot.constants import BigBrother as BigBrotherConfig, Guild as GuildConfig, Icons from bot.pagination import LinePaginator from bot.utils import messages from bot.utils.time import time_since @@ -22,36 +24,26 @@ URL_RE = re.compile(r"(https?://[^\s]+)") def proxy_user(user_id: str) -> Object: + """A proxy user object that mocks a real User instance for when the later is not available.""" try: user_id = int(user_id) except ValueError: raise BadArgument + user = Object(user_id) user.mention = user.id user.display_name = f"<@{user.id}>" user.avatar_url_as = lambda static_format: None user.bot = False + return user class WatchChannel(ABC): - """ - Base class for WatchChannels - - Abstracts the basic functionality for watchchannels in - a granular manner to allow for easy overwritting of - methods in the child class. - """ + """ABC that implements watch channel functionality to relay all messages of a user to a watch channel.""" @abstractmethod def __init__(self, bot: Bot, destination, webhook_id, api_endpoint, api_default_params, logger) -> None: - """ - abstractmethod for __init__ which should still be called with super(). - - Note: Some of the attributes below need to be overwritten in the - __init__ of the child after the super().__init__(*args, **kwargs) - call. - """ self.bot = bot self.destination = destination # E.g., Channels.big_brother_logs @@ -74,6 +66,11 @@ class WatchChannel(ABC): self._start = self.bot.loop.create_task(self.start_watchchannel()) @property + def modlog(self) -> ModLog: + """Provides access to the ModLog cog for alert purposes.""" + return self.bot.get_cog("ModLog") + + @property def consuming_messages(self) -> bool: """Checks if a consumption task is currently running.""" if self._consume_task is None: @@ -83,7 +80,7 @@ class WatchChannel(ABC): exc = self._consume_task.exception() if exc: self.log.exception( - f"{self.__class__.__name__} consume task has failed with:", + f"The message queue consume task has failed with:", exc_info=exc ) return False @@ -91,52 +88,58 @@ class WatchChannel(ABC): return True async def start_watchchannel(self) -> None: - """Retrieves watched users from the API.""" + """Starts the watch channel by getting the channel, webhook, and user cache ready.""" await self.bot.wait_until_ready() - if await self.initialize_channel() and await self.fetch_user_cache(): - self.log.trace(f"Started the {self.__class__.__name__} WatchChannel") + # After updating d.py, this block can be replaced by `fetch_channel` with a try-except + for attempt in range(1, self.retries+1): + self.channel = self.bot.get_channel(self.destination) + if self.channel is None: + if attempt < self.retries: + await asyncio.sleep(self.retry_delay) + else: + break else: - self.log.error(f"Failed to start the {self.__class__.__name__} WatchChannel") + self.log.error(f"Failed to retrieve the text channel with id `{self.destination}") - # Let's try again in a minute. - await asyncio.sleep(60) - self._start = self.bot.loop.create_task(self.start_watchchannel()) + # `get_webhook_info` has been renamed to `fetch_webhook` in newer versions of d.py + try: + self.webhook = await self.bot.get_webhook_info(self.webhook_id) + except (discord.HTTPException, discord.NotFound, discord.Forbidden): + self.log.exception(f"Failed to fetch webhook with id `{self.webhook_id}`") - async def initialize_channel(self) -> bool: - """ - Checks if channel and webhook are set; if not, tries to initialize them. + if self.channel is None or self.webhook is None: + self.log.error("Failed to start the watch channel; unloading the cog.") - Since the internal channel cache may not be available directly after `ready`, - this function will retry to get the channel a number of times. If both the - channel and webhook were initialized successfully. this function will return - `True`. - """ - if self.channel is None: - for attempt in range(1, self.retries + 1): - self.channel = self.bot.get_channel(self.destination) - - if self.channel is None: - self.log.error(f"Failed to get the {self.__class__.__name__} channel; cannot watch users") - if attempt < self.initialization_retries: - self.log.error(f"Attempt {attempt}/{self.retries}; Retrying in {self.retry_delay} seconds...") - await asyncio.sleep(self.retry_delay) - else: - self.log.trace(f"Retrieved the TextChannel for {self.__class__.__name__}") - break - else: - self.log.error(f"Cannot get channel with id `{self.destination}`; cannot watch users") - return False + message = textwrap.dedent( + f""" + An error occurred while loading the text channel or webhook. - if self.webhook is None: - self.webhook = await self.bot.get_webhook_info(self.webhook_id) # This is `fetch_webhook` in current - if self.webhook is None: - self.log.error(f"Cannot get webhook with id `{self.webhook_id}`; cannot watch users") - return False - self.log.trace(f"Retrieved the webhook for {self.__class__.__name__}") + TextChannel: {"**Failed to load**" if self.channel is None else "Loaded successfully"} + Webhook: {"**Failed to load**" if self.webhook is None else "Loaded successfully"} - self.log.trace(f"WatchChannel for {self.__class__.__name__} is fully initialized") - return True + The Cog has been unloaded.""" + ) + + await self.modlog.send_log_message( + title=f"Error: Failed to initialize the {self.__class__.__name__} watch channel", + text=message, + ping_everyone=False, + icon_url=Icons.token_removed, + colour=Color.red() + ) + + self.bot.remove_cog(self.__class__.__name__) + return + + if not await self.fetch_user_cache(): + await self.modlog.send_log_message( + title=f"Warning: Failed to retrieve user cache for the {self.__class__.__name__} watch channel", + text="Could not retrieve the list of watched users from the API and messages will not be relayed.", + ping_everyone=True, + icon=Icons.token_removed, + color=Color.red() + ) async def fetch_user_cache(self) -> bool: """ @@ -145,15 +148,9 @@ class WatchChannel(ABC): This function returns `True` if the update succeeded. """ try: - data = await self.bot.api_client.get( - self.api_endpoint, - params=self.api_default_params - ) + data = await self.bot.api_client.get(self.api_endpoint, params=self.api_default_params) except aiohttp.ClientResponseError as e: - self.log.exception( - f"Failed to fetch {self.__class__.__name__} watched users from API", - exc_info=e - ) + self.log.exception(f"Failed to fetch the watched users from the API", exc_info=e) return False self.watched_users = defaultdict(dict) @@ -181,7 +178,7 @@ class WatchChannel(ABC): self.log.trace(f"{self.__class__.__name__} started consuming the message queue") - # Prevent losing a partly processed consumption queue after Task failure + # If the previous consumption Task failed, first consume the existing comsumption_queue if not self.consumption_queue: self.consumption_queue = self.message_queue.copy() self.message_queue.clear() @@ -198,15 +195,16 @@ class WatchChannel(ABC): if self.message_queue: self.log.trace("Channel queue not empty: Continuing consuming queues") - self._consume_task = self.bot.loop.create_task( - self.consume_messages(delay_consumption=False) - ) + self._consume_task = self.bot.loop.create_task(self.consume_messages(delay_consumption=False)) else: self.log.trace("Done consuming messages.") async def webhook_send( - self, content: Optional[str] = None, username: Optional[str] = None, - avatar_url: Optional[str] = None, embed: Optional[Embed] = None, + self, + content: Optional[str] = None, + username: Optional[str] = None, + avatar_url: Optional[str] = None, + embed: Optional[Embed] = None, ) -> None: """Sends a message to the webhook with the specified kwargs.""" try: @@ -218,7 +216,7 @@ class WatchChannel(ABC): ) async def relay_message(self, msg: Message) -> None: - """Relays the message to the relevant WatchChannel""" + """Relays the message to the relevant watch channel""" last_author, last_channel, count = self.message_history limit = BigBrotherConfig.header_message_limit @@ -230,7 +228,7 @@ class WatchChannel(ABC): cleaned_content = msg.clean_content if cleaned_content: - # Put all non-media URLs in a codeblock to prevent embeds + # Put all non-media URLs in a code block to prevent embeds media_urls = {embed.url for embed in msg.embeds if embed.type in ("image", "video")} for url in URL_RE.findall(cleaned_content): if url not in media_urls: @@ -263,7 +261,7 @@ class WatchChannel(ABC): self.message_history[2] += 1 async def send_header(self, msg) -> None: - """Sends a header embed to the WatchChannel""" + """Sends a header embed with information about the relayed messages to the watch channel""" user_id = msg.author.id guild = self.bot.get_guild(GuildConfig.id) @@ -275,18 +273,10 @@ class WatchChannel(ABC): reason = self.watched_users[user_id]['reason'] - embed = Embed(description=( - f"{msg.author.mention} in [#{msg.channel.name}]({msg.jump_url})\n" - )) - embed.set_footer(text=( - f"Added {time_delta} by {actor} | " - f"Reason: {reason}" - )) - await self.webhook_send( - embed=embed, - username=msg.author.display_name, - avatar_url=msg.author.avatar_url - ) + embed = Embed(description=f"{msg.author.mention} in [#{msg.channel.name}]({msg.jump_url})") + embed.set_footer(text=f"Added {time_delta} by {actor} | Reason: {reason}") + + await self.webhook_send(embed=embed, username=msg.author.display_name, avatar_url=msg.author.avatar_url) async def list_watched_users(self, ctx: Context, update_cache: bool = True) -> None: """ @@ -310,15 +300,12 @@ class WatchChannel(ABC): time_delta = self._get_time_delta(inserted_at) lines.append(f"• <@{user_id}> (added {time_delta})") - await LinePaginator.paginate( - lines or ("There's nothing here yet.",), - ctx, - Embed( - title=f"{self.__class__.__name__} watched users ({'updated' if update_cache else 'cached'})", - color=Color.blue() - ), - empty=False + lines = lines or ("There's nothing here yet.",) + embed = Embed( + title=f"{self.__class__.__name__} watched users ({'updated' if update_cache else 'cached'})", + color=Color.blue() ) + await LinePaginator.paginate(lines, ctx, embed, empty=False) @staticmethod def _get_time_delta(time_string: str) -> str: @@ -346,7 +333,7 @@ class WatchChannel(ABC): self.consumption_queue.pop(user_id, None) def cog_unload(self) -> None: - """Takes care of unloading the cog and cancelling the consumption task.""" + """Takes care of unloading the cog and canceling the consumption task.""" self.log.trace(f"Unloading {self.__class__._name__} cog") if not self._consume_task.done(): self._consume_task.cancel() @@ -354,6 +341,6 @@ class WatchChannel(ABC): self._consume_task.result() except asyncio.CancelledError as e: self.log.exception( - f"The {self.__class__._name__} consume task was cancelled. Messages may be lost.", + f"The {self.__class__._name__} consume task was canceled. Messages may be lost.", exc_info=e ) diff --git a/bot/utils/messages.py b/bot/utils/messages.py index b285444c2..5c9b5b4d7 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -80,7 +80,7 @@ async def wait_for_deletion( async def send_attachments(message: Message, destination: Union[TextChannel, Webhook]): """ - Re-uploads each attachment in a message to the given channel. + Re-uploads each attachment in a message to the given channel or webhook. Each attachment is sent as a separate message to more easily comply with the 8 MiB request size limit. If attachments are too large, they are instead grouped into a single embed which links to them. |