diff options
author | 2021-03-30 11:32:11 -0700 | |
---|---|---|
committer | 2021-03-30 11:49:52 -0700 | |
commit | b71a1e5d595d0775ffc1b4f502b9fc5efc3ca18d (patch) | |
tree | 8c49e2554e57044e34b675be0b4978f077426f1c | |
parent | Update arrow to 1.0.3 (diff) |
HelpChannels: use aware datetimes everywhere
Fix issues converting timestamps to datetimes and vice-versa. The main
culprit id `datetime.timestamp()`, which always assumes naïve objects
are in local time. That behaviour conflicts with discord.py, which
returns naïve objects in UTC rather than local time. Switching from
`utcfromtimestamp` to `fromtimestamp` was incorrect since the latter
also assumes the timestamp is in local time.
-rw-r--r-- | bot/exts/help_channels/_channel.py | 25 | ||||
-rw-r--r-- | bot/exts/help_channels/_cog.py | 18 | ||||
-rw-r--r-- | bot/exts/help_channels/_message.py | 18 |
3 files changed, 34 insertions, 27 deletions
diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index b1960531d..719d341bd 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -1,8 +1,10 @@ import logging import typing as t -from datetime import datetime, timedelta +from datetime import timedelta +import arrow import discord +from arrow import Arrow import bot from bot import constants @@ -25,8 +27,8 @@ def get_category_channels(category: discord.CategoryChannel) -> t.Iterable[disco yield channel -async def get_closing_time(channel: discord.TextChannel, init_done: bool) -> t.Tuple[datetime, str]: - """Return the timestamp at which the given help `channel` should be closed along with the reason.""" +async def get_closing_time(channel: discord.TextChannel, init_done: bool) -> t.Tuple[Arrow, str]: + """Return the time at which the given help `channel` should be closed along with the reason.""" log.trace(f"Getting the closing time for #{channel} ({channel.id}).") is_empty = await _message.is_empty(channel) @@ -49,23 +51,24 @@ async def get_closing_time(channel: discord.TextChannel, init_done: bool) -> t.T msg = await _message.get_last_message(channel) if not msg: - # last message can't be retreived, return datetime.min so channel closes right now. + # Last message can't be retrieved, return datetime.min so channel closes right now. log.debug(f"No idle time available; #{channel} ({channel.id}) has no messages, closing now.") - return datetime.min, "deleted" + return Arrow.min, "deleted" # The time at which a channel should be closed. - return msg.created_at + timedelta(minutes=idle_minutes_claimant), "latest_message" + time = Arrow.fromdatetime(msg.created_at) + timedelta(minutes=idle_minutes_claimant) + return time, "latest_message" # Switch to datetime objects so we can use time deltas - claimant_last_message_time = datetime.fromtimestamp(claimant_last_message_time) + claimant_last_message_time = Arrow.utcfromtimestamp(claimant_last_message_time) non_claimant_last_message_time = await _caches.non_claimant_last_message_times.get(channel.id) if non_claimant_last_message_time: - non_claimant_last_message_time = datetime.fromtimestamp(non_claimant_last_message_time) + non_claimant_last_message_time = Arrow.utcfromtimestamp(non_claimant_last_message_time) else: # If it's falsey, then it indicates a non-claimant has yet to reply to this session. # Set to min date time so it isn't considered when calculating the closing time. - non_claimant_last_message_time = datetime.min + non_claimant_last_message_time = Arrow.min # Get the later time at which a channel should be closed non_claimant_last_message_time += timedelta(minutes=constants.HelpChannels.idle_minutes_others) @@ -92,8 +95,8 @@ async def get_in_use_time(channel_id: int) -> t.Optional[timedelta]: claimed_timestamp = await _caches.claim_times.get(channel_id) if claimed_timestamp: - claimed = datetime.fromtimestamp(claimed_timestamp) - return datetime.utcnow() - claimed + claimed = Arrow.utcfromtimestamp(claimed_timestamp) + return arrow.utcnow() - claimed def is_excluded_channel(channel: discord.abc.GuildChannel) -> bool: diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 0e71661ac..832c9cd84 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -2,9 +2,10 @@ import asyncio import logging import random import typing as t -from datetime import datetime, timedelta +from datetime import timedelta from operator import attrgetter +import arrow import discord import discord.abc from discord.ext import commands @@ -72,7 +73,7 @@ class HelpChannels(commands.Cog): self.channel_queue: asyncio.Queue[discord.TextChannel] = None self.name_queue: t.Deque[str] = None - self.last_notification: t.Optional[datetime] = None + self.last_notification: t.Optional[arrow.Arrow] = None # Asyncio stuff self.queue_tasks: t.List[asyncio.Task] = [] @@ -114,9 +115,12 @@ class HelpChannels(commands.Cog): self.bot.stats.incr("help.claimed") - await _caches.claim_times.set(message.channel.id, message.created_at.timestamp()) - await _caches.claimant_last_message_times.set(message.channel.id, message.created_at.timestamp()) - # Reset thie non_claimant cache for this channel to indicate that this session has yet to be answered. + # datetime.timestamp() would assume it's local, despite d.py giving a (naïve) UTC time. + timestamp = arrow.Arrow.fromdatetime(message.created_at).timestamp() + + await _caches.claim_times.set(message.channel.id, timestamp) + await _caches.claimant_last_message_times.set(message.channel.id, timestamp) + # Delete to indicate that the help session has yet to receive an answer. await _caches.non_claimant_last_message_times.delete(message.channel.id) # Not awaited because it may indefinitely hold the lock while waiting for a channel. @@ -298,7 +302,7 @@ class HelpChannels(commands.Cog): # Closing time is in the past. # Add 1 second due to POSIX timestamps being lower resolution than datetime objects. - if closing_time < (datetime.utcnow() + timedelta(seconds=1)): + if closing_time < (arrow.utcnow() + timedelta(seconds=1)): log.info( f"#{channel} ({channel.id}) is idle past {closing_time} " @@ -311,7 +315,7 @@ class HelpChannels(commands.Cog): if has_task: self.scheduler.cancel(channel.id) - delay = (closing_time - datetime.utcnow()).seconds + delay = (closing_time - arrow.utcnow()).seconds log.info( f"#{channel} ({channel.id}) is still active; " f"scheduling it to be moved after {delay} seconds." diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index d60b31dea..afd698ffe 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -1,9 +1,10 @@ import logging import textwrap import typing as t -from datetime import datetime +import arrow import discord +from arrow import Arrow import bot from bot import constants @@ -51,13 +52,12 @@ async def update_message_caches(message: discord.Message) -> None: 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 - # Use datetime naive time stamp to be consistant with timestamps from discord. - timestamp = 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: @@ -128,12 +128,12 @@ async def dm_on_open(message: discord.Message) -> None: ) -async def notify(channel: discord.TextChannel, last_notification: t.Optional[datetime]) -> t.Optional[datetime]: +async def notify(channel: discord.TextChannel, last_notification: t.Optional[Arrow]) -> t.Optional[Arrow]: """ Send a message in `channel` notifying about a lack of available help channels. - If a notification was sent, return the `datetime` at which the message was sent. Otherwise, - return None. + If a notification was sent, return the time at which the message was sent. + Otherwise, return None. Configuration: @@ -147,7 +147,7 @@ async def notify(channel: discord.TextChannel, last_notification: t.Optional[dat log.trace("Notifying about lack of channels.") if last_notification: - elapsed = (datetime.utcnow() - last_notification).seconds + elapsed = (arrow.utcnow() - last_notification).seconds minimum_interval = constants.HelpChannels.notify_minutes * 60 should_send = elapsed >= minimum_interval else: @@ -170,7 +170,7 @@ async def notify(channel: discord.TextChannel, last_notification: t.Optional[dat allowed_mentions=discord.AllowedMentions(everyone=False, roles=allowed_roles) ) - return message.created_at + return Arrow.fromdatetime(message.created_at) except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about lack of dormant channels!") |