diff options
Diffstat (limited to '')
| -rw-r--r-- | bot/constants.py | 2 | ||||
| -rw-r--r-- | bot/exts/help_channels/_channel.py | 61 | ||||
| -rw-r--r-- | bot/exts/help_channels/_cog.py | 9 | ||||
| -rw-r--r-- | config-default.yml | 7 | 
4 files changed, 78 insertions, 1 deletions
| diff --git a/bot/constants.py b/bot/constants.py index 24862059e..9851aea97 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -615,6 +615,8 @@ class HelpChannels(metaclass=YAMLGetter):      section = 'help_channels'      enable: bool +    idle_minutes: int +    deleted_idle_minutes: int      cmd_whitelist: List[int] diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index 3a290bdfd..48dcc13c6 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -1,9 +1,11 @@  """Contains all logic to handle changes to posts in the help forum."""  import asyncio  import textwrap +from datetime import timedelta +import arrow  import discord -from pydis_core.utils import members +from pydis_core.utils import members, scheduling  import bot  from bot import constants @@ -168,3 +170,60 @@ async def help_post_deleted(deleted_post_event: discord.RawThreadDeleteEvent) ->      if cached_post and not cached_post.archived:          # If the post is in the bot's cache, and it was not archived before deleting, report a complete session.          await _stats.report_complete_session(cached_post, _stats.ClosingReason.DELETED) + + +async def get_closing_time(post: discord.Thread) -> tuple[arrow.Arrow, _stats.ClosingReason]: +    """ +    Return the time at which the given help `post` should be closed along with the reason. + +    The time is calculated by first checking if the opening message is deleted. +    If it is, then get the last 100 messages (the most that can be fetched in one API call). +    If less than 100 message are returned, and none are from the post owner, then assume the poster +        has sent no further messages and close deleted_idle_minutes after the post creation time. + +    Otherwise, use the most recent message's create_at date and add `idle_minutes_claimant`. +    """ +    try: +        starter_message = post.starter_message or await post.fetch_message(post.id) +    except discord.NotFound: +        starter_message = None + +    last_100_messages = [message async for message in post.history(limit=100, oldest_first=False)] + +    if starter_message is None and len(last_100_messages) < 100: +        if not discord.utils.get(last_100_messages, author__id=post.owner_id): +            time = arrow.Arrow.fromdatetime(post.created_at) +            time += timedelta(minutes=constants.HelpChannels.deleted_idle_minutes) +            return time, _stats.ClosingReason.DELETED + +    time = arrow.Arrow.fromdatetime(last_100_messages[0].created_at) +    time += timedelta(minutes=constants.HelpChannels.idle_minutes) +    return time, _stats.ClosingReason.INACTIVE + + +async def maybe_archive_idle_post(post: discord.Thread, scheduler: scheduling.Scheduler, has_task: bool = True) -> None: +    """ +    Archive the `post` if idle, or schedule the archive for later if still active. + +    If `has_task` is True and rescheduling is required, the extant task to make the post +    dormant will first be cancelled. +    """ +    log.trace(f"Handling open post #{post} ({post.id}).") + +    closing_time, closing_reason = await get_closing_time(post) + +    if closing_time < (arrow.utcnow() + timedelta(seconds=1)): +        # Closing time is in the past. +        # Add 1 second due to POSIX timestamps being lower resolution than datetime objects. +        log.info( +            f"#{post} ({post.id}) is idle past {closing_time} and will be archived. Reason: {closing_reason.value}" +        ) +        await _close_help_post(post, closing_reason) +        return + +    if has_task: +        scheduler.cancel(post.id) +    delay = (closing_time - arrow.utcnow()).seconds +    log.info(f"#{post} ({post.id}) is still active; scheduling it to be archived after {delay} seconds.") + +    scheduler.schedule_later(delay, post.id, maybe_archive_idle_post(post, scheduler, has_task=True)) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 742cf38dd..0c3510c29 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -4,6 +4,7 @@ import typing as t  import discord  from discord.ext import commands +from pydis_core.utils import scheduling  from bot import constants  from bot.bot import Bot @@ -28,13 +29,21 @@ class HelpForum(commands.Cog):      def __init__(self, bot: Bot):          self.bot = bot +        self.scheduler = scheduling.Scheduler(self.__class__.__name__)          self.help_forum_channel: discord.ForumChannel = None +    async def cog_unload(self) -> None: +        """Cancel all scheduled tasks on unload.""" +        self.scheduler.cancel_all() +      async def cog_load(self) -> None:          """Archive all idle open posts, schedule check for later for active open posts."""          log.trace("Initialising help forum cog.")          self.help_forum_channel = self.bot.get_channel(constants.Channels.help_system_forum) +        for post in self.help_forum_channel.channels: +            await _channel.maybe_archive_idle_post(post, self.scheduler, has_task=False) +      async def close_check(self, ctx: commands.Context) -> bool:          """Return True if the channel is a help post, and the user is the claimant or has a whitelisted role."""          if not _channel.is_help_forum_post(ctx.channel): diff --git a/config-default.yml b/config-default.yml index c9d043ff7..1d7a2ff78 100644 --- a/config-default.yml +++ b/config-default.yml @@ -493,6 +493,13 @@ free:  help_channels:      enable: true +    # Allowed duration of inactivity before archiving a help post +    idle_minutes: 30 + +    # Allowed duration of inactivity when post is empty (due to deleted messages) +    # before archiving a help post +    deleted_idle_minutes: 5 +      # Roles which are allowed to use the command which makes channels dormant      cmd_whitelist:          - *HELPERS_ROLE | 
