aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/constants.py2
-rw-r--r--bot/exts/help_channels/_caches.py9
-rw-r--r--bot/exts/help_channels/_cog.py78
-rw-r--r--bot/exts/help_channels/_message.py27
-rw-r--r--bot/utils/messages.py2
-rw-r--r--bot/utils/scheduling.py19
-rw-r--r--config-default.yml4
7 files changed, 121 insertions, 20 deletions
diff --git a/bot/constants.py b/bot/constants.py
index ab55da482..3d960f22b 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -433,6 +433,8 @@ class Channels(metaclass=YAMLGetter):
off_topic_1: int
off_topic_2: int
+ black_formatter: int
+
bot_commands: int
discord_py: int
esoteric: int
diff --git a/bot/exts/help_channels/_caches.py b/bot/exts/help_channels/_caches.py
index c5e4ee917..8d45c2466 100644
--- a/bot/exts/help_channels/_caches.py
+++ b/bot/exts/help_channels/_caches.py
@@ -24,3 +24,12 @@ question_messages = RedisCache(namespace="HelpChannels.question_messages")
# This cache keeps track of the dynamic message ID for
# the continuously updated message in the #How-to-get-help channel.
dynamic_message = RedisCache(namespace="HelpChannels.dynamic_message")
+
+# This cache keeps track of who has help-dms on.
+# RedisCache[discord.User.id, bool]
+help_dm = RedisCache(namespace="HelpChannels.help_dm")
+
+# This cache tracks member who are participating and opted in to help channel dms.
+# serialise the set as a comma separated string to allow usage with redis
+# RedisCache[discord.TextChannel.id, str[set[discord.User.id]]]
+session_participants = RedisCache(namespace="HelpChannels.session_participants")
diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py
index 5c410a0a1..7fb72d7ef 100644
--- a/bot/exts/help_channels/_cog.py
+++ b/bot/exts/help_channels/_cog.py
@@ -424,6 +424,7 @@ class HelpChannels(commands.Cog):
) -> None:
"""Actual implementation of `unclaim_channel`. See that for full documentation."""
await _caches.claimants.delete(channel.id)
+ await _caches.session_participants.delete(channel.id)
claimant = self.bot.get_guild(constants.Guild.id).get_member(claimant_id)
if claimant is None:
@@ -466,7 +467,9 @@ class HelpChannels(commands.Cog):
if channel_utils.is_in_category(message.channel, constants.Categories.help_available):
if not _channel.is_excluded_channel(message.channel):
await self.claim_channel(message)
- else:
+
+ elif channel_utils.is_in_category(message.channel, constants.Categories.help_in_use):
+ await self.notify_session_participants(message)
await _message.update_message_caches(message)
@commands.Cog.listener()
@@ -535,3 +538,76 @@ class HelpChannels(commands.Cog):
)
self.dynamic_message = new_dynamic_message["id"]
await _caches.dynamic_message.set("message_id", self.dynamic_message)
+
+ @staticmethod
+ def _serialise_session_participants(participants: set[int]) -> str:
+ """Convert a set to a comma separated string."""
+ return ','.join(str(p) for p in participants)
+
+ @staticmethod
+ def _deserialise_session_participants(s: str) -> set[int]:
+ """Convert a comma separated string into a set."""
+ return set(int(user_id) for user_id in s.split(",") if user_id != "")
+
+ @lock.lock_arg(NAMESPACE, "message", attrgetter("channel.id"))
+ @lock.lock_arg(NAMESPACE, "message", attrgetter("author.id"))
+ async def notify_session_participants(self, message: discord.Message) -> None:
+ """
+ Check if the message author meets the requirements to be notified.
+
+ If they meet the requirements they are notified.
+ """
+ if await _caches.claimants.get(message.channel.id) == message.author.id:
+ return # Ignore messages sent by claimants
+
+ if not await _caches.help_dm.get(message.author.id):
+ return # Ignore message if user is opted out of help dms
+
+ if (await self.bot.get_context(message)).command == self.close_command:
+ return # Ignore messages that are closing the channel
+
+ session_participants = self._deserialise_session_participants(
+ await _caches.session_participants.get(message.channel.id) or ""
+ )
+
+ if message.author.id not in session_participants:
+ session_participants.add(message.author.id)
+
+ embed = discord.Embed(
+ title="Currently Helping",
+ description=f"You're currently helping in {message.channel.mention}",
+ color=constants.Colours.soft_green,
+ timestamp=message.created_at
+ )
+ embed.add_field(name="Conversation", value=f"[Jump to message]({message.jump_url})")
+ await message.author.send(embed=embed)
+
+ await _caches.session_participants.set(
+ message.channel.id,
+ self._serialise_session_participants(session_participants)
+ )
+
+ @commands.command(name="helpdm")
+ async def helpdm_command(
+ self,
+ ctx: commands.Context,
+ state_bool: bool
+ ) -> None:
+ """
+ Allows user to toggle "Helping" dms.
+
+ If this is set to on the user will receive a dm for the channel they are participating in.
+
+ If this is set to off the user will not receive a dm for channel that they are participating in.
+ """
+ state_str = "ON" if state_bool else "OFF"
+
+ if state_bool == await _caches.help_dm.get(ctx.author.id, False):
+ await ctx.send(f"{constants.Emojis.cross_mark} {ctx.author.mention} Help DMs are already {state_str}")
+ return
+
+ if state_bool:
+ await _caches.help_dm.set(ctx.author.id, True)
+ else:
+ await _caches.help_dm.delete(ctx.author.id)
+ await ctx.send(f"{constants.Emojis.ok_hand} {ctx.author.mention} Help DMs {state_str}!")
diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py
index afd698ffe..4c7c39764 100644
--- a/bot/exts/help_channels/_message.py
+++ b/bot/exts/help_channels/_message.py
@@ -9,7 +9,6 @@ from arrow import Arrow
import bot
from bot import constants
from bot.exts.help_channels import _caches
-from bot.utils.channel import is_in_category
log = logging.getLogger(__name__)
@@ -47,23 +46,21 @@ async def update_message_caches(message: discord.Message) -> None:
"""Checks the source of new content in a help channel and updates the appropriate cache."""
channel = message.channel
- # Confirm the channel is an in use help channel
- if is_in_category(channel, constants.Categories.help_in_use):
- log.trace(f"Checking if #{channel} ({channel.id}) has had a reply.")
+ log.trace(f"Checking if #{channel} ({channel.id}) has had a reply.")
- claimant_id = await _caches.claimants.get(channel.id)
- if not claimant_id:
- # The mapping for this channel doesn't exist, we can't do anything.
- return
+ claimant_id = await _caches.claimants.get(channel.id)
+ if not claimant_id:
+ # The mapping for this channel doesn't exist, we can't do anything.
+ return
- # datetime.timestamp() would assume it's local, despite d.py giving a (naïve) UTC time.
- timestamp = Arrow.fromdatetime(message.created_at).timestamp()
+ # datetime.timestamp() would assume it's local, despite d.py giving a (naïve) UTC time.
+ timestamp = Arrow.fromdatetime(message.created_at).timestamp()
- # Overwrite the appropriate last message cache depending on the author of the message
- if message.author.id == claimant_id:
- await _caches.claimant_last_message_times.set(channel.id, timestamp)
- else:
- await _caches.non_claimant_last_message_times.set(channel.id, timestamp)
+ # Overwrite the appropriate last message cache depending on the author of the message
+ if message.author.id == claimant_id:
+ await _caches.claimant_last_message_times.set(channel.id, timestamp)
+ else:
+ await _caches.non_claimant_last_message_times.set(channel.id, timestamp)
async def get_last_message(channel: discord.TextChannel) -> t.Optional[discord.Message]:
diff --git a/bot/utils/messages.py b/bot/utils/messages.py
index b6f6c1f66..d4a921161 100644
--- a/bot/utils/messages.py
+++ b/bot/utils/messages.py
@@ -54,7 +54,7 @@ def reaction_check(
log.trace(f"Removing reaction {reaction} by {user} on {reaction.message.id}: disallowed user.")
scheduling.create_task(
reaction.message.remove_reaction(reaction.emoji, user),
- HTTPException, # Suppress the HTTPException if adding the reaction fails
+ suppressed_exceptions=(HTTPException,),
name=f"remove_reaction-{reaction}-{reaction.message.id}-{user}"
)
return False
diff --git a/bot/utils/scheduling.py b/bot/utils/scheduling.py
index 2dc485f24..bb83b5c0d 100644
--- a/bot/utils/scheduling.py
+++ b/bot/utils/scheduling.py
@@ -161,9 +161,22 @@ class Scheduler:
self._log.error(f"Error in task #{task_id} {id(done_task)}!", exc_info=exception)
-def create_task(coro: t.Awaitable, *suppressed_exceptions: t.Type[Exception], **kwargs) -> asyncio.Task:
- """Wrapper for `asyncio.create_task` which logs exceptions raised in the task."""
- task = asyncio.create_task(coro, **kwargs)
+def create_task(
+ coro: t.Awaitable,
+ *,
+ suppressed_exceptions: tuple[t.Type[Exception]] = (),
+ event_loop: t.Optional[asyncio.AbstractEventLoop] = None,
+ **kwargs,
+) -> asyncio.Task:
+ """
+ Wrapper for creating asyncio `Task`s which logs exceptions raised in the task.
+
+ If the loop kwarg is provided, the task is created from that event loop, otherwise the running loop is used.
+ """
+ if event_loop is not None:
+ task = event_loop.create_task(coro, **kwargs)
+ else:
+ task = asyncio.create_task(coro, **kwargs)
task.add_done_callback(partial(_log_task_exception, suppressed_exceptions=suppressed_exceptions))
return task
diff --git a/config-default.yml b/config-default.yml
index 55388247c..48fd7c47e 100644
--- a/config-default.yml
+++ b/config-default.yml
@@ -176,6 +176,9 @@ guild:
user_log: 528976905546760203
voice_log: 640292421988646961
+ # Open Source Projects
+ black_formatter: &BLACK_FORMATTER 846434317021741086
+
# Off-topic
off_topic_0: 291284109232308226
off_topic_1: 463035241142026251
@@ -244,6 +247,7 @@ guild:
reminder_whitelist:
- *BOT_CMD
- *DEV_CONTRIB
+ - *BLACK_FORMATTER
roles:
announcements: 463658397560995840