calendar polish
This commit is contained in:
1 parent
1dcd53a581
commit
61de4c3933
2 files changed
+79
-33
No files matched your search
+25
-33
@@ -1,8 +1,8 @@
|
|||||||
"""Calendar cog — book events via Discord slash commands, displayed on the website."""
|
"""Calendar cog — book events via Discord slash commands; website auto-converts timezones."""
|
||||||
######################################################################
|
######################################################################
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
@@ -19,6 +19,8 @@ FIRESTORE_CALENDAR = (
|
|||||||
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||||
TIME_RE = re.compile(r"^\d{2}:\d{2}$")
|
TIME_RE = re.compile(r"^\d{2}:\d{2}$")
|
||||||
|
|
||||||
|
from .calendar_data import COUNTRY_TZ, tz_choices
|
||||||
|
|
||||||
|
|
||||||
class Calendar(commands.Cog):
|
class Calendar(commands.Cog):
|
||||||
"""Book and manage calendar events from Discord."""
|
"""Book and manage calendar events from Discord."""
|
||||||
@@ -27,55 +29,53 @@ class Calendar(commands.Cog):
|
|||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
async def cog_load(self) -> None:
|
async def cog_load(self) -> None:
|
||||||
"""Sync slash commands when the cog loads."""
|
|
||||||
try:
|
try:
|
||||||
await self.bot.tree.sync()
|
await self.bot.tree.sync()
|
||||||
log.info("Calendar slash commands synced")
|
log.info("Calendar slash commands synced")
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning("Failed to sync slash commands", exc_info=True)
|
log.warning("Failed to sync slash commands", exc_info=True)
|
||||||
|
|
||||||
# ── /book ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@app_commands.command(name="book", description="Add an event to the calendar")
|
@app_commands.command(name="book", description="Add an event to the calendar")
|
||||||
@app_commands.describe(
|
@app_commands.describe(
|
||||||
date="Date in YYYY-MM-DD format (e.g. 2026-07-20)",
|
date="Date: YYYY-MM-DD (e.g. 2026-07-20)",
|
||||||
time="Time in HH:MM 24-hour format (optional, e.g. 14:30)",
|
time="Time: HH:MM 24-hour format (e.g. 14:30)",
|
||||||
|
country="What country's time is this? (default: Turkey)",
|
||||||
title="Event title",
|
title="Event title",
|
||||||
description="Extra notes (optional)",
|
description="Extra notes (optional)",
|
||||||
)
|
)
|
||||||
|
@app_commands.choices(country=tz_choices())
|
||||||
async def book_cmd(
|
async def book_cmd(
|
||||||
self,
|
self,
|
||||||
interaction: discord.Interaction,
|
interaction: discord.Interaction,
|
||||||
date: str,
|
date: str,
|
||||||
|
time: str,
|
||||||
title: str,
|
title: str,
|
||||||
time: str = "",
|
country: str = "Turkey",
|
||||||
description: str = "",
|
description: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Slash command: /book <date> <title> [time] [description]"""
|
|
||||||
await interaction.response.defer()
|
await interaction.response.defer()
|
||||||
|
|
||||||
if not DATE_RE.match(date):
|
if not DATE_RE.match(date) or not TIME_RE.match(time):
|
||||||
return await interaction.followup.send(
|
return await interaction.followup.send("❌ Format: `/book date:2026-07-20 time:14:30 country:Turkey title:Event`")
|
||||||
"❌ Invalid date. Use `YYYY-MM-DD`, e.g. `2026-07-20`."
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
datetime.strptime(date, "%Y-%m-%d")
|
datetime.strptime(date, "%Y-%m-%d")
|
||||||
|
datetime.strptime(time, "%H:%M")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return await interaction.followup.send("❌ That date doesn't exist.")
|
return await interaction.followup.send("❌ Invalid date or time.")
|
||||||
|
|
||||||
if time and not TIME_RE.match(time):
|
offset = COUNTRY_TZ.get(country, 3)
|
||||||
return await interaction.followup.send("❌ Time must be `HH:MM` (24-hour), e.g. `14:30`.")
|
tz = timezone(timedelta(hours=offset))
|
||||||
if time:
|
local_dt = datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M").replace(tzinfo=tz)
|
||||||
try:
|
utc_ts = local_dt.astimezone(timezone.utc).isoformat()
|
||||||
datetime.strptime(time, "%H:%M")
|
|
||||||
except ValueError:
|
|
||||||
return await interaction.followup.send("❌ Invalid time.")
|
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"fields": {
|
"fields": {
|
||||||
"title": {"stringValue": title.strip()},
|
"title": {"stringValue": title.strip()},
|
||||||
"date": {"stringValue": date},
|
"date": {"stringValue": date},
|
||||||
"time": {"stringValue": time},
|
"time": {"stringValue": time},
|
||||||
|
"country": {"stringValue": country},
|
||||||
|
"tzOffset": {"doubleValue": offset},
|
||||||
|
"utc": {"stringValue": utc_ts},
|
||||||
"description": {"stringValue": description.strip()},
|
"description": {"stringValue": description.strip()},
|
||||||
"createdBy": {"stringValue": str(interaction.user)},
|
"createdBy": {"stringValue": str(interaction.user)},
|
||||||
}
|
}
|
||||||
@@ -84,24 +84,19 @@ class Calendar(commands.Cog):
|
|||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.post(FIRESTORE_CALENDAR, json=payload) as resp:
|
async with session.post(FIRESTORE_CALENDAR, json=payload) as resp:
|
||||||
if resp.status in (200, 201):
|
if resp.status in (200, 201):
|
||||||
when = date
|
|
||||||
if time:
|
|
||||||
when += f" at {time}"
|
|
||||||
desc = f" — {description}" if description else ""
|
desc = f" — {description}" if description else ""
|
||||||
await interaction.followup.send(
|
await interaction.followup.send(
|
||||||
f"📅 Booked **{when}**: **{title}**{desc}"
|
f"📅 Booked **{date} at {time} ({country})**: **{title}**{desc}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
body = await resp.text()
|
body = await resp.text()
|
||||||
log.warning("Firestore POST failed %d: %s", resp.status, body[:200])
|
log.warning("Firestore POST failed %d: %s", resp.status, body[:200])
|
||||||
await interaction.followup.send("❌ Failed to save event.")
|
await interaction.followup.send("❌ Failed to save event.")
|
||||||
|
|
||||||
# ── /unbook ──────────────────────────────────────────────────────────
|
@app_commands.command(name="unbook", description="Remove an event")
|
||||||
|
|
||||||
@app_commands.command(name="unbook", description="Remove an event from the calendar")
|
|
||||||
@app_commands.describe(
|
@app_commands.describe(
|
||||||
date="Date of the event (YYYY-MM-DD)",
|
date="Date of the event (YYYY-MM-DD)",
|
||||||
title="Exact title of the event to remove",
|
title="Exact title to remove",
|
||||||
)
|
)
|
||||||
async def unbook_cmd(
|
async def unbook_cmd(
|
||||||
self,
|
self,
|
||||||
@@ -109,13 +104,10 @@ class Calendar(commands.Cog):
|
|||||||
date: str,
|
date: str,
|
||||||
title: str,
|
title: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Slash command: /unbook <date> <title>"""
|
|
||||||
await interaction.response.defer()
|
await interaction.response.defer()
|
||||||
|
|
||||||
if not DATE_RE.match(date) or not title.strip():
|
if not DATE_RE.match(date) or not title.strip():
|
||||||
return await interaction.followup.send(
|
return await interaction.followup.send("❌ Usage: `/unbook date:2026-07-20 title:Event`")
|
||||||
"❌ Usage: `/unbook YYYY-MM-DD Title`"
|
|
||||||
)
|
|
||||||
|
|
||||||
deleted = False
|
deleted = False
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Country → UTC offset mapping for the calendar cog."""
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
COUNTRY_TZ: dict[str, float] = {
|
||||||
|
# Europe
|
||||||
|
"Turkey": 3, "UK": 0,
|
||||||
|
"Germany": 2, "France": 2,
|
||||||
|
"Netherlands": 2, "Belgium": 2,
|
||||||
|
"Poland": 2, "Italy": 2,
|
||||||
|
"Spain": 2, "Sweden": 2,
|
||||||
|
"Norway": 2, "Denmark": 2,
|
||||||
|
"Switzerland": 2, "Austria": 2,
|
||||||
|
"Czechia": 2, "Greece": 3,
|
||||||
|
"Finland": 3, "Romania": 3,
|
||||||
|
"Ukraine": 3, "Portugal": 1,
|
||||||
|
"Ireland": 1, "Iceland": 0,
|
||||||
|
|
||||||
|
# Americas
|
||||||
|
"US East": -5, "US Central": -6,
|
||||||
|
"US Mountain": -7, "US West": -8,
|
||||||
|
"US Alaska": -9, "US Hawaii": -10,
|
||||||
|
"Canada East": -5, "Canada Central": -6,
|
||||||
|
"Canada West": -8, "Mexico": -6,
|
||||||
|
"Brazil": -3, "Argentina": -3,
|
||||||
|
"Colombia": -5, "Chile": -4,
|
||||||
|
|
||||||
|
# Asia / Pacific
|
||||||
|
"Japan": 9, "Korea": 9,
|
||||||
|
"China": 8, "Taiwan": 8,
|
||||||
|
"Hong Kong": 8, "Singapore": 8,
|
||||||
|
"Malaysia": 8, "Philippines": 8,
|
||||||
|
"Indonesia West": 7, "Thailand": 7,
|
||||||
|
"Vietnam": 7, "India": 5.5,
|
||||||
|
"Pakistan": 5, "Bangladesh": 6,
|
||||||
|
"Iran": 3.5, "Israel": 3,
|
||||||
|
"Saudi Arabia": 3, "UAE": 4,
|
||||||
|
|
||||||
|
# Oceania
|
||||||
|
"Australia East": 11, "Australia Central": 10.5,
|
||||||
|
"Australia West": 8, "New Zealand": 13,
|
||||||
|
|
||||||
|
# Africa
|
||||||
|
"South Africa": 2, "Egypt": 3,
|
||||||
|
"Nigeria": 1, "Kenya": 3,
|
||||||
|
"Morocco": 1,
|
||||||
|
}
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
|
def tz_choices() -> list:
|
||||||
|
"""Return app_commands.Choice list for the country dropdown."""
|
||||||
|
from discord import app_commands
|
||||||
|
return [app_commands.Choice(name=c, value=c) for c in sorted(COUNTRY_TZ)]
|
||||||
Reference in new issue
Block a user