use timezone-aware datetimes everywhere

This commit is contained in:
leander committed 2026-10-24 18:14:43 +02:00
1 parent a2cc923f13
commit d6bdc4926c
12 files changed
+115 -49

No files matched your search

+3 -1
View File
@@ -6,6 +6,7 @@ from discord import app_commands
from discord.ext import commands
from cogs.utils import embed_templates
from cogs.utils import misc_utils
class Anime(commands.Cog):
@@ -300,7 +301,8 @@ class Anime(commands.Cog):
year, month, day = date.get("year"), date.get("month"), date.get("day")
if year and month and day:
return discord.utils.format_dt(datetime(year, month, day), style="d")
date = misc_utils.localize(datetime(year, month, day), self.bot.timezone)
return discord.utils.format_dt(date, style="d")
return f"{day or '?'}.{month or '?'}.{year or '?'}"
def construct_release_schedule_string(self, start_date: dict, end_date: dict) -> str:
+4 -4
View File
@@ -21,7 +21,7 @@ class Aurora(commands.Cog):
self.bot = bot
self.notified = datetime(2000, 9, 11) # Used to prevent spamming aurora alerts
self.notified = datetime(2000, 9, 11, tzinfo=timezone.utc) # Used to prevent spamming aurora alerts
self.aurora_alarm.start()
def cog_unload(self):
@@ -46,7 +46,7 @@ class Aurora(commands.Cog):
self.bot.logger.warning("Failed to get forecast data")
return None
now = datetime.now(timezone.utc)
now = discord.utils.utcnow()
valid_sightings = []
for interval in data["shortIntervals"]:
start = datetime.fromisoformat(interval["start"])
@@ -82,7 +82,7 @@ class Aurora(commands.Cog):
await self.bot.wait_until_ready() # Make sure we can fetch the #general channel
if datetime.now() - self.notified < timedelta(hours=12):
if discord.utils.utcnow() - self.notified < timedelta(hours=12):
self.bot.logger.info(
"aurora_alarm: Not sending aurora alert because it has been less than 12 hours since last alert"
)
@@ -121,7 +121,7 @@ class Aurora(commands.Cog):
content="SJANSE FOR NORDLYS AKKURAT NÅ PÅ NEUF! Se ut vinduet istedenfor på den dumme skjermen din",
embed=embed,
)
self.notified = datetime.now() # Update the last notification time
self.notified = discord.utils.utcnow() # Update the last notification time
async def setup(bot: commands.Bot):
+42 -18
View File
@@ -61,19 +61,23 @@ class Birthday(commands.Cog):
self.bot.logger.info("Checking for birthdays")
birthdays = await self.bot.db.fetchall("""
today = self.local_today()
birthdays = await self.bot.db.fetchall(
"""
SELECT *
FROM birthdays
WHERE (
EXTRACT(MONTH FROM birthday) = EXTRACT(MONTH FROM current_date)
AND EXTRACT(DAY FROM birthday) = EXTRACT(DAY FROM current_date)
EXTRACT(MONTH FROM birthday) = EXTRACT(MONTH FROM %(today)s)
AND EXTRACT(DAY FROM birthday) = EXTRACT(DAY FROM %(today)s)
) OR (
-- Celebrate 29 February birthdays on 28 February in years that aren't leap years
to_char(birthday, 'MM-DD') = '02-29'
AND to_char(current_date, 'MM-DD') = '02-28'
AND to_char(current_date + 1, 'MM-DD') = '03-01'
AND to_char(%(today)s, 'MM-DD') = '02-28'
AND to_char(%(today)s + 1, 'MM-DD') = '03-01'
);
""")
""",
{"today": today.date()},
)
self.bot.logger.info(f"Found the following birthdays: {birthdays}")
@@ -93,7 +97,7 @@ class Birthday(commands.Cog):
continue
date = datetime(birthday[1].year, birthday[1].month, birthday[1].day)
years_old = relativedelta(datetime.now(), date).years
years_old = relativedelta(today, date).years
# Is it inefficent to define a list of greetings per iteration? Yes
# Am I hoping python somehow will automatically optimize my code? Yes
@@ -144,6 +148,16 @@ class Birthday(commands.Cog):
self.bot.logger.info("Unloading cog")
self.birthday_check.cancel()
def local_today(self) -> datetime:
"""
Returns
----------
(datetime): Midnight today in the bot's time zone, as a naive datetime to compare with birthdays
"""
now = discord.utils.utcnow().astimezone(self.bot.timezone)
return datetime(now.year, now.month, now.day)
async def fetch_user_birthday(self, user_id: int) -> datetime | None:
"""
Fetch the birthday of a user in the database
@@ -177,18 +191,19 @@ class Birthday(commands.Cog):
results = await self.bot.db.fetchone(
"""
SELECT *, CAST(birthday + ((EXTRACT(YEAR FROM AGE(birthday)) + 1) * interval '1' YEAR) AS DATE) AS next_bday
SELECT *,
CAST(birthday + ((EXTRACT(YEAR FROM AGE(%s, birthday)) + 1) * interval '1' YEAR) AS DATE) AS next_bday
FROM birthdays
WHERE discord_id = %s
""",
(user_id,),
(self.local_today().date(), user_id),
)
if results:
birthday = results[2]
return datetime(birthday.year, birthday.month, birthday.day)
else:
# This will never happen unless a database error occurs. Keeping the type checker happy
return datetime.now()
return self.local_today()
async def fetch_next_birthdays(self) -> list[tuple[int, datetime, datetime]]:
"""
@@ -199,11 +214,15 @@ class Birthday(commands.Cog):
(list[tuple[int, datetime, datetime]]): List of tuples containing the user ID, date of birth and next birthday
"""
results = await self.bot.db.fetchall("""
SELECT *, CAST(birthday + ((EXTRACT(YEAR FROM AGE(birthday)) + 1) * interval '1' YEAR) AS DATE) AS next_bday
results = await self.bot.db.fetchall(
"""
SELECT *,
CAST(birthday + ((EXTRACT(YEAR FROM AGE(%s, birthday)) + 1) * interval '1' YEAR) AS DATE) AS next_bday
FROM birthdays
ORDER BY next_bday ASC
""")
""",
(self.local_today().date(),),
)
# Convert date objects to datetime objects
birthdays = []
@@ -268,7 +287,7 @@ class Birthday(commands.Cog):
ephemeral=True,
)
if date > datetime.now():
if date > self.local_today():
return await interaction.response.send_message(
embed=embed_templates.error_warning("Du kan ikke sette en fremtidig dato!"),
ephemeral=True,
@@ -276,6 +295,7 @@ class Birthday(commands.Cog):
await self.set_user_birthday(interaction.user.id, date)
date = misc_utils.localize(date, self.bot.timezone)
embed = embed_templates.success(f"Bursdag satt til {discord.utils.format_dt(date, style='D')}")
await interaction.response.send_message(embed=embed)
@@ -322,13 +342,14 @@ class Birthday(commands.Cog):
)
# Current age information
today = self.local_today()
birthday = datetime(user.year, user.month, user.day)
days_old = (datetime.now() - birthday).days
years_old = relativedelta(datetime.now(), birthday).years
days_old = (today - birthday).days
years_old = relativedelta(today, birthday).years
# Next birthday information
next_birthday = await self.fetch_user_next_birthday(bruker.id)
next_birthday_days = (next_birthday.date() - datetime.now().date()).days
next_birthday_days = (next_birthday - today).days
# Japanese era
japanese_era = EraDate.from_date(birthday).strftime("%-K%-y年")
@@ -336,7 +357,9 @@ class Birthday(commands.Cog):
embed = discord.Embed(description=bruker.mention, color=bruker.color)
embed.set_thumbnail(url=bruker.display_avatar)
embed.set_author(name=bruker.name, icon_url=bruker.display_avatar)
embed.add_field(name="Bursdag", value=discord.utils.format_dt(birthday, style="D"))
embed.add_field(
name="Bursdag", value=discord.utils.format_dt(misc_utils.localize(birthday, self.bot.timezone), style="D")
)
embed.add_field(name="Hvor gammel?", value=f"{years_old} år\n({days_old} dager)", inline=False)
embed.add_field(name="Weeb age", value=japanese_era, inline=False)
embed.add_field(
@@ -378,6 +401,7 @@ class Birthday(commands.Cog):
except (discord.NotFound, discord.HTTPException):
continue
next_birthday = misc_utils.localize(next_birthday, self.bot.timezone)
timestamp = discord.utils.format_dt(next_birthday, style="D")
relative_timestamp = discord.utils.format_dt(next_birthday, style="R")
birthday_strings.append(f"* {discord_user.name} - {timestamp} ({relative_timestamp})")
+3 -4
View File
@@ -1,7 +1,6 @@
import asyncio
import json
import random
from datetime import datetime
from datetime import timedelta
from io import BytesIO
from typing import override
@@ -46,7 +45,7 @@ class CS2Bingo(commands.Cog):
"""
if lobby := self.active_lobbies.get(str(interaction.user.id), None):
if lobby.ends < datetime.now():
if lobby.ends < discord.utils.utcnow():
self.active_lobbies.pop(str(interaction.user.id))
else:
return await interaction.response.send_message(
@@ -54,7 +53,7 @@ class CS2Bingo(commands.Cog):
)
for lobby in self.active_lobbies.values():
if lobby.ends > datetime.now() and any(p.id == interaction.user.id for p in lobby.players):
if lobby.ends > discord.utils.utcnow() and any(p.id == interaction.user.id for p in lobby.players):
return await interaction.response.send_message(
embed=embed_templates.error_warning("Du er allerede i en lobby")
)
@@ -62,7 +61,7 @@ class CS2Bingo(commands.Cog):
self.active_lobbies[str(interaction.user.id)] = Lobby(
host=interaction.user,
players=[interaction.user],
ends=datetime.now() + timedelta(minutes=10),
ends=discord.utils.utcnow() + timedelta(minutes=10),
kicked_players=[],
)
+4 -3
View File
@@ -1,6 +1,7 @@
import random
import re
from datetime import datetime
from datetime import timezone
import discord
from discord.ext import commands
@@ -21,7 +22,7 @@ class FunReplies(commands.Cog):
# This is kind of a shitty way to do it but I'm too lazy to implement anything good right now
self.cooldown_seconds = 600
initial_datetime = datetime(
2000, 9, 11
2000, 9, 11, tzinfo=timezone.utc
) # Set initial datetime far in the past to allow triggering right after boot
self.previous_invokations = {
"olof palme": initial_datetime,
@@ -101,12 +102,12 @@ class FunReplies(commands.Cog):
bool: Whether or not the reply was triggered
"""
if (datetime.now() - self.previous_invokations[cooldown_key]).total_seconds() < self.cooldown_seconds:
if (discord.utils.utcnow() - self.previous_invokations[cooldown_key]).total_seconds() < self.cooldown_seconds:
return False
if re.search(regex_match, message.content, flags=regex_flags) and random.randint(1, 100) <= trigger_chance:
await message.reply(reply)
self.previous_invokations[cooldown_key] = datetime.now()
self.previous_invokations[cooldown_key] = discord.utils.utcnow()
return True
return False
+2
View File
@@ -5,6 +5,7 @@ from discord import app_commands
from discord.ext import commands
from cogs.utils import embed_templates
from cogs.utils import misc_utils
class Galtinn(commands.Cog):
@@ -113,6 +114,7 @@ class Galtinn(commands.Cog):
membership_status = "Ja" if last_membership["is_valid"] else "Nei"
if last_membership["membership_type"] != "lifelong":
expiration_date = datetime.strptime(last_membership["end_date"], "%Y-%m-%d")
expiration_date = misc_utils.localize(expiration_date, self.bot.timezone)
expires = discord.utils.format_dt(expiration_date, "D")
time_left = discord.utils.format_dt(expiration_date, "R")
+8 -2
View File
@@ -14,6 +14,7 @@ from PIL import ImageFont
from cogs.utils import discord_utils
from cogs.utils import embed_templates
from cogs.utils import misc_utils
class Misc(commands.Cog):
@@ -214,7 +215,7 @@ class Misc(commands.Cog):
"""
land = land.upper()
år = datetime.now().year if not år else int(år)
år = discord.utils.utcnow().astimezone(self.bot.timezone).year if not år else int(år)
async with self.bot.http_session.get(f"https://date.nager.at/api/v3/publicholidays/{år}/{land}") as response:
if response.status not in (200, 204):
@@ -231,7 +232,8 @@ class Misc(commands.Cog):
# Construct output
holiday_str = ""
for day in data:
date = discord.utils.format_dt(datetime.strptime(day["date"], "%Y-%m-%d"), style="D")
date = datetime.strptime(day["date"], "%Y-%m-%d")
date = discord.utils.format_dt(misc_utils.localize(date, self.bot.timezone), style="D")
holiday_str += f'* **{date}**: {day["localName"]}\n'
embed = discord.Embed(
@@ -358,6 +360,10 @@ class Misc(commands.Cog):
parsed_time = dateparser.parse(
tekst,
languages=["nb", "nn", "en"],
settings={
"TIMEZONE": str(self.bot.timezone) if self.bot.timezone else "local",
"RETURN_AS_TIMEZONE_AWARE": True,
},
)
if not parsed_time:
embed = embed_templates.error_warning("Forstod ikke tidspunktet. Prøv å skrive det på en annen måte.")
+4 -4
View File
@@ -1,5 +1,4 @@
import random
from datetime import datetime
import discord
from discord.ext import commands
@@ -41,7 +40,7 @@ class RepeatedMessages(commands.Cog):
Sends a message on Friday at 00:00
"""
if datetime.now().weekday() != 4:
if discord.utils.utcnow().astimezone(self.bot.timezone).weekday() != 4:
return
if not (channel := self.get_general_channel()):
@@ -73,7 +72,7 @@ class RepeatedMessages(commands.Cog):
"""
Sends a message on Monday at 00:00
"""
if datetime.now().weekday() != 0:
if discord.utils.utcnow().astimezone(self.bot.timezone).weekday() != 0:
return
if not (channel := self.get_general_channel()):
return
@@ -90,7 +89,8 @@ class RepeatedMessages(commands.Cog):
Sends a message on New Year's Day at 00:00
"""
if datetime.now().month != 1 or datetime.now().day != 1:
today = discord.utils.utcnow().astimezone(self.bot.timezone)
if today.month != 1 or today.day != 1:
return
if not (channel := self.get_general_channel()):
+17 -1
View File
@@ -50,10 +50,26 @@ class StarboardScraper(commands.Cog):
CREATE TABLE IF NOT EXISTS starboard_scraper_state (
id INTEGER PRIMARY KEY DEFAULT 1,
last_scraped_message_id BIGINT,
last_scrape_time TIMESTAMP,
last_scrape_time TIMESTAMPTZ,
CHECK (id = 1)
)
""")
await self.bot.db.execute("""
DO $$
BEGIN
IF (
SELECT data_type
FROM information_schema.columns
WHERE table_schema = current_schema()
AND table_name = 'starboard_scraper_state'
AND column_name = 'last_scrape_time'
) = 'timestamp without time zone' THEN
ALTER TABLE starboard_scraper_state
ALTER COLUMN last_scrape_time TYPE TIMESTAMPTZ
USING last_scrape_time AT TIME ZONE 'Europe/Oslo';
END IF;
END $$;
""")
async def get_last_scraped_message_id(self):
"""
+3 -4
View File
@@ -6,7 +6,6 @@ Below you will see a lot of stupid shit. Pls ignore
"""
import random
from datetime import datetime
from datetime import timedelta
from typing import override
@@ -47,7 +46,7 @@ class TenMan(commands.Cog):
"""
if lobby := self.active_lobbies.get(str(interaction.user.id), None):
if lobby.ends < datetime.now():
if lobby.ends < discord.utils.utcnow():
self.active_lobbies.pop(str(interaction.user.id))
else:
return await interaction.response.send_message(
@@ -57,7 +56,7 @@ class TenMan(commands.Cog):
)
for lobby in self.active_lobbies.values():
if lobby.ends > datetime.now() and any(p.id == interaction.user.id for p in lobby.players):
if lobby.ends > discord.utils.utcnow() and any(p.id == interaction.user.id for p in lobby.players):
return await interaction.response.send_message(
embed=embed_templates.error_warning(
"Du er allerede i en lobby",
@@ -67,7 +66,7 @@ class TenMan(commands.Cog):
self.active_lobbies[str(interaction.user.id)] = Lobby(
host=interaction.user,
players=[interaction.user],
ends=datetime.now() + timedelta(minutes=30),
ends=discord.utils.utcnow() + timedelta(minutes=30),
kicked_players=[],
)
+8 -8
View File
@@ -191,7 +191,7 @@ class Lobby:
class LobbyView(discord.ui.View):
def __init__(self, lobby: Lobby, bot: commands.Bot):
lobby_end = (lobby.ends - datetime.now()).total_seconds()
lobby_end = (lobby.ends - discord.utils.utcnow()).total_seconds()
super().__init__(timeout=lobby_end)
self.lobby = lobby
@@ -210,7 +210,7 @@ class LobbyView(discord.ui.View):
pass
async def end_lobby(self):
self.lobby.ends = datetime.now()
self.lobby.ends = discord.utils.utcnow()
for item in self.children:
item.disabled = True
@@ -225,7 +225,7 @@ class LobbyView(discord.ui.View):
@discord.ui.button(label="Bli med", style=discord.ButtonStyle.blurple)
async def join_lobby(self, interaction: discord.Interaction, button: discord.ui.Button):
if datetime.now() > self.lobby.ends:
if discord.utils.utcnow() > self.lobby.ends:
embed = embed_templates.error_warning("Lobbyen har allerede startet")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
@@ -289,7 +289,7 @@ class LobbyView(discord.ui.View):
embed = embed_templates.error_warning("Bare hosten kan slette lobbyen")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
self.lobby.ends = datetime.now()
self.lobby.ends = discord.utils.utcnow()
await interaction.message.delete()
embed = embed_templates.success("Lobbyen har blitt slettet")
@@ -418,7 +418,7 @@ class TempVoiceHelper:
continue
if len(channel.members) == 0:
self.temp_vc_channels[channel]["no_members_since"] = datetime.now()
self.temp_vc_channels[channel]["no_members_since"] = discord.utils.utcnow()
self.bot.logger.info(f"Temporary voice channel {channel} has no members. Will be deleted in 1 minutes")
else:
self.temp_vc_channels[channel]["no_members_since"] = None
@@ -434,7 +434,7 @@ class TempVoiceHelper:
if not data["no_members_since"]:
continue
if (datetime.now() - data["no_members_since"]).total_seconds() >= 60:
if (discord.utils.utcnow() - data["no_members_since"]).total_seconds() >= 60:
try:
await channel.delete(reason="tempvoice kanal inaktiv i 1 minutt")
except discord.NotFound:
@@ -470,7 +470,7 @@ class TempVoiceHelper:
await self.forget(channel_id)
continue
now = datetime.now()
now = discord.utils.utcnow()
self.temp_vc_channels[channel] = {"created": now, "no_members_since": None if channel.members else now}
restored += 1
@@ -514,7 +514,7 @@ class TempVoiceHelper:
self.bot.logger.info(f"Created temporary voice channel {channel} in {interaction.guild}")
now = datetime.now()
now = discord.utils.utcnow()
self.temp_vc_channels[channel] = {"created": now, "no_members_since": now}
await self.remember(channel)
+17
View File
@@ -16,6 +16,23 @@ from PIL import ImageFont
MIDNIGHT = datetime.time(hour=0, minute=0, tzinfo=ZoneInfo("Europe/Oslo"))
def localize(dt: datetime.datetime, tz: datetime.tzinfo | None) -> datetime.datetime:
"""
Attach a time zone to a naive datetime without changing its wall time
Parameters
----------
dt (datetime.datetime): The naive datetime
tz (datetime.tzinfo | None): The time zone. None means the system time zone
Returns
----------
(datetime.datetime): The aware datetime
"""
return dt.replace(tzinfo=tz) if tz else dt.astimezone()
# The exceptions discord.py's tasks.loop retries instead of stopping on
RETRIED_TASK_EXCEPTIONS = (
OSError,