aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/cogs/watchchannels/bigbrother.py22
-rw-r--r--bot/cogs/watchchannels/talentpool.py11
-rw-r--r--bot/cogs/watchchannels/watchchannel.py173
-rw-r--r--bot/utils/messages.py2
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.