aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar MarkKoz <[email protected]>2021-03-30 11:32:11 -0700
committerGravatar MarkKoz <[email protected]>2021-03-30 11:49:52 -0700
commitb71a1e5d595d0775ffc1b4f502b9fc5efc3ca18d (patch)
tree8c49e2554e57044e34b675be0b4978f077426f1c
parentUpdate 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.py25
-rw-r--r--bot/exts/help_channels/_cog.py18
-rw-r--r--bot/exts/help_channels/_message.py18
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!")