diff options
| -rw-r--r-- | bot/constants.py | 2 | ||||
| -rw-r--r-- | bot/exts/help_channels/_caches.py | 9 | ||||
| -rw-r--r-- | bot/exts/help_channels/_cog.py | 78 | ||||
| -rw-r--r-- | bot/exts/help_channels/_message.py | 27 | ||||
| -rw-r--r-- | bot/utils/messages.py | 2 | ||||
| -rw-r--r-- | bot/utils/scheduling.py | 19 | ||||
| -rw-r--r-- | config-default.yml | 4 | 
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 | 
