new timestamp feature and make tz data shared across cogs that need it
This commit is contained in:
1 parent
7d754367ae
commit
7018366068
7 files changed
+428
-23
No files matched your search
@@ -10,6 +10,9 @@ it started as a simple music bot and became a massive mess of features i actuall
|
|||||||
- **media & music** — yt-dlp + ffmpeg based music playback. also has commands for random ffmpeg media conversions.
|
- **media & music** — yt-dlp + ffmpeg based music playback. also has commands for random ffmpeg media conversions.
|
||||||
- **sysinfo** — live monitoring of my laptop's cpu, ram, temps (lm-sensors), top processes, and fastfetch.
|
- **sysinfo** — live monitoring of my laptop's cpu, ram, temps (lm-sensors), top processes, and fastfetch.
|
||||||
- **utilities** — live currency conversion, steam stat tracking (strinova player counts).
|
- **utilities** — live currency conversion, steam stat tracking (strinova player counts).
|
||||||
|
- **calendar** — firestore-backed event booking via modal. autoconverts local times to UTC using a country list, feeds a web calendar frontend.
|
||||||
|
- **timestamp** — `/timestamp` modal that generates Discord `<t:unix:FORMAT>` tags. accepts day numbers or day names ("Sunday"), handles month rollover, outputs all 7 Discord time formats.
|
||||||
|
- **timestamp (friends)** — passive message listener: when a whitelisted user says "my time" in chat, the bot parses the surrounding text for time expressions, auto-converts to their timezone, and replies with timestamp tags. no commands needed.
|
||||||
- **junk** — dice, coinflip, aura meter, rank, gifs, copypastas.
|
- **junk** — dice, coinflip, aura meter, rank, gifs, copypastas.
|
||||||
|
|
||||||
## stack
|
## stack
|
||||||
@@ -30,3 +33,5 @@ run these to see the sub-commands:
|
|||||||
- `!gifs` — the gif list
|
- `!gifs` — the gif list
|
||||||
- `!fun` — the junk commands
|
- `!fun` — the junk commands
|
||||||
- `!gacha help` — blue archive recruitment simulator (pick banners, pull with live rates)
|
- `!gacha help` — blue archive recruitment simulator (pick banners, pull with live rates)
|
||||||
|
- `!book` / `!unbook` — calendar event booking (whitelisted users only)
|
||||||
|
- `/timestamp` — Discord timestamp generator modal
|
||||||
+2
-19
@@ -10,7 +10,7 @@ import discord
|
|||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
from firebase_website import get_db
|
from firebase_website import get_db
|
||||||
from .calendar_data import COUNTRY_TZ
|
from .tz_data import COUNTRY_TZ, find_offset
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -22,23 +22,6 @@ TR_TZ = timezone(timedelta(hours=3))
|
|||||||
WH_NAME = "koharu"
|
WH_NAME = "koharu"
|
||||||
|
|
||||||
|
|
||||||
def _find_offset(country: str) -> float | None:
|
|
||||||
"""Look up a country's UTC offset. Case-insensitive fuzzy match."""
|
|
||||||
c = country.strip().lower()
|
|
||||||
if not c:
|
|
||||||
return None
|
|
||||||
for name, offset in COUNTRY_TZ.items():
|
|
||||||
if c == name.lower():
|
|
||||||
return offset
|
|
||||||
for name, offset in COUNTRY_TZ.items():
|
|
||||||
if name.lower().startswith(c):
|
|
||||||
return offset
|
|
||||||
for name, offset in COUNTRY_TZ.items():
|
|
||||||
if c in name.lower():
|
|
||||||
return offset
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# BOOK MODAL
|
# BOOK MODAL
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@@ -130,7 +113,7 @@ class BookModal(discord.ui.Modal, title="Book an Event"):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# --- timezone lookup ---
|
# --- timezone lookup ---
|
||||||
offset = _find_offset(country)
|
offset = find_offset(country)
|
||||||
if offset is None:
|
if offset is None:
|
||||||
return await interaction.response.send_message(
|
return await interaction.response.send_message(
|
||||||
"Unknown country. Try: Turkey, UK, US East, Japan, etc.",
|
"Unknown country. Try: Turkey, UK, US East, Japan, etc.",
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
"""Timestamp converter cog — generate Discord timestamp markdown via modal.
|
||||||
|
######################################################################
|
||||||
|
Usage:
|
||||||
|
/timestamp → opens a popup form (day, time, country)
|
||||||
|
and returns all Discord <t:unix:FORMAT> variants.
|
||||||
|
"""
|
||||||
|
import calendar as cal_mod
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from .tz_data import find_offset
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DAY_RE = re.compile(r"^\d{1,2}$") # "4" or "14"
|
||||||
|
DAY_NAME_RE = re.compile(r"^(mon|tue|wed|thu|fri|sat|sun|monday|tuesday|wednesday|thursday|friday|saturday|sunday)$", re.IGNORECASE)
|
||||||
|
TIME_RE = re.compile(r"^\d{2}:\d{2}$") # "14:30"
|
||||||
|
|
||||||
|
_WEEKDAY: dict[str, int] = {
|
||||||
|
"mon": 0, "tue": 1, "wed": 2, "thu": 3, "fri": 4, "sat": 5, "sun": 6,
|
||||||
|
"monday": 0, "tuesday": 1, "wednesday": 2, "thursday": 3,
|
||||||
|
"friday": 4, "saturday": 5, "sunday": 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _all_formats(unix_ts: int) -> list[tuple[str, str]]:
|
||||||
|
"""Return every Discord timestamp format for *unix_ts*."""
|
||||||
|
return [
|
||||||
|
("Short Time", f"<t:{unix_ts}:t>"),
|
||||||
|
("Long Time", f"<t:{unix_ts}:T>"),
|
||||||
|
("Short Date", f"<t:{unix_ts}:d>"),
|
||||||
|
("Long Date", f"<t:{unix_ts}:D>"),
|
||||||
|
("Short Date+Time", f"<t:{unix_ts}:f>"),
|
||||||
|
("Long Date+Time", f"<t:{unix_ts}:F>"),
|
||||||
|
("Relative", f"<t:{unix_ts}:R>"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# MODAL
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
class TimestampModal(discord.ui.Modal, title="Timestamp Converter"):
|
||||||
|
"""Popup form: day of month, 24h time, and a country for timezone."""
|
||||||
|
|
||||||
|
day = discord.ui.TextInput(
|
||||||
|
label="Day (number or name)",
|
||||||
|
placeholder="e.g. 4 or Sunday",
|
||||||
|
min_length=2, max_length=9,
|
||||||
|
)
|
||||||
|
time = discord.ui.TextInput(
|
||||||
|
label="Time (24h, HH:MM)",
|
||||||
|
placeholder="e.g. 14:30",
|
||||||
|
min_length=4, max_length=5,
|
||||||
|
)
|
||||||
|
country = discord.ui.TextInput(
|
||||||
|
label="Country / Timezone",
|
||||||
|
placeholder="e.g. Turkey, Japan, US East …",
|
||||||
|
default="Turkey",
|
||||||
|
max_length=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_submit(self, interaction: discord.Interaction) -> None:
|
||||||
|
day_raw = self.day.value.strip()
|
||||||
|
time_str = self.time.value.strip()
|
||||||
|
country = self.country.value.strip()
|
||||||
|
|
||||||
|
# --- day: number or day name ---
|
||||||
|
day_int: int | None = None
|
||||||
|
if DAY_RE.match(day_raw):
|
||||||
|
day_int = int(day_raw)
|
||||||
|
if not (1 <= day_int <= 31):
|
||||||
|
return await interaction.response.send_message(
|
||||||
|
"Day must be between 1 and 31.", ephemeral=True,
|
||||||
|
)
|
||||||
|
elif not DAY_NAME_RE.match(day_raw):
|
||||||
|
return await interaction.response.send_message(
|
||||||
|
"Day must be a number (1-31) or day name like Sunday, Mon, etc.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- validate time ---
|
||||||
|
if not TIME_RE.match(time_str):
|
||||||
|
return await interaction.response.send_message(
|
||||||
|
"Time must be HH:MM (24h), e.g. 14:30", ephemeral=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
parsed_time = datetime.strptime(time_str, "%H:%M")
|
||||||
|
except ValueError:
|
||||||
|
return await interaction.response.send_message(
|
||||||
|
"Invalid time value.", ephemeral=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- timezone lookup ---
|
||||||
|
offset = find_offset(country)
|
||||||
|
if offset is None:
|
||||||
|
return await interaction.response.send_message(
|
||||||
|
"Unknown country. Try: Turkey, UK, US East, Japan, etc.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
tz = timezone(timedelta(hours=offset))
|
||||||
|
|
||||||
|
# --- resolve month / year ---
|
||||||
|
now = datetime.now(tz)
|
||||||
|
if day_int is not None:
|
||||||
|
# --- numeric day: same rollover logic as book ---
|
||||||
|
if day_int < now.day:
|
||||||
|
next_month = now.month + 1
|
||||||
|
if next_month > 12:
|
||||||
|
month_int = 1
|
||||||
|
year_int = now.year + 1
|
||||||
|
else:
|
||||||
|
month_int = next_month
|
||||||
|
year_int = now.year
|
||||||
|
else:
|
||||||
|
month_int = now.month
|
||||||
|
year_int = now.year
|
||||||
|
# --- make sure the day exists in the resolved month ---
|
||||||
|
max_days = cal_mod.monthrange(year_int, month_int)[1]
|
||||||
|
if day_int > max_days:
|
||||||
|
month_name = datetime(year_int, month_int, 1).strftime("%B")
|
||||||
|
return await interaction.response.send_message(
|
||||||
|
f"Day {day_int} doesn't exist in {month_name} {year_int}.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# --- day name: find next occurrence of that weekday ---
|
||||||
|
target_wd = _WEEKDAY[day_raw.lower()]
|
||||||
|
today_wd = now.weekday()
|
||||||
|
days_ahead = (target_wd - today_wd) % 7
|
||||||
|
target_date = now + timedelta(days=days_ahead)
|
||||||
|
day_int = target_date.day
|
||||||
|
month_int = target_date.month
|
||||||
|
year_int = target_date.year
|
||||||
|
|
||||||
|
# --- build datetime & unix timestamp ---
|
||||||
|
local_dt = datetime(
|
||||||
|
year_int, month_int, day_int,
|
||||||
|
parsed_time.hour, parsed_time.minute,
|
||||||
|
).replace(tzinfo=tz)
|
||||||
|
unix_ts = int(local_dt.astimezone(timezone.utc).timestamp())
|
||||||
|
|
||||||
|
# --- output all formats ---
|
||||||
|
lines = [
|
||||||
|
f"**{local_dt.strftime('%d %B at %H:%M')}**",
|
||||||
|
f"Unix: `{unix_ts}`",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for label, tag in _all_formats(unix_ts):
|
||||||
|
lines.append(f"`{tag}` ← {label}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("*Copy the tag you want — times display in each viewer's local timezone.*")
|
||||||
|
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"\n".join(lines),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# COG
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
class Timestamp(commands.Cog):
|
||||||
|
"""Slash command that opens the timestamp-converter modal."""
|
||||||
|
|
||||||
|
def __init__(self, bot: commands.Bot) -> None:
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@app_commands.command(name="timestamp", description="Convert a date+time into Discord timestamp markdown")
|
||||||
|
async def timestamp_cmd(self, interaction: discord.Interaction) -> None:
|
||||||
|
await interaction.response.send_modal(TimestampModal())
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# setup
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot) -> None:
|
||||||
|
await bot.add_cog(Timestamp(bot))
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
"""Timestamp converter for friends — detects "my time" in messages.
|
||||||
|
######################################################################
|
||||||
|
No slash commands. When a hardcoded friend writes *my time* anywhere
|
||||||
|
in their message the bot parses the surrounding text for time expressions,
|
||||||
|
converts them to the friend's local timezone, and replies with ready-to-use
|
||||||
|
Discord <t:unix:FORMAT> markdown.
|
||||||
|
|
||||||
|
Requires: pip install dateparser
|
||||||
|
Friend map: TIMESTAMP_FRIEND_{DISCORD_USER_ID}=Country in credentials.env
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from .tz_data import find_offset
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# optional dependency
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
try:
|
||||||
|
import dateparser
|
||||||
|
from dateparser.search import search_dates as _search_dates
|
||||||
|
HAS_DATEPARSER = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_DATEPARSER = False
|
||||||
|
log.warning(
|
||||||
|
"timestamp_friends: dateparser not installed; "
|
||||||
|
"install with `pip install dateparser`"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# trigger phrase & morning-indicator regex
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
_TRIGGER_RE = re.compile(r"my time", re.IGNORECASE)
|
||||||
|
|
||||||
|
# Morning indicators that prevent the PM default from kicking in.
|
||||||
|
# (?:\b|(?<=\d)) handles "3 am" (space), "3am" (digit-prefixed), and
|
||||||
|
# "a.m." at sentence start — \b alone fails when am/a.m. follows a digit.
|
||||||
|
_MORNING_RE = re.compile(
|
||||||
|
r"(?:\b|(?<=\d))(?:am|a\.m|morning|dawn|sunrise|early\s*morning)\b",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _all_formats(unix_ts: int) -> list[tuple[str, str]]:
|
||||||
|
"""Return every Discord timestamp format for *unix_ts*."""
|
||||||
|
return [
|
||||||
|
("Short Time", f"<t:{unix_ts}:t>"),
|
||||||
|
("Long Time", f"<t:{unix_ts}:T>"),
|
||||||
|
("Short Date", f"<t:{unix_ts}:d>"),
|
||||||
|
("Long Date", f"<t:{unix_ts}:D>"),
|
||||||
|
("Short Date+Time", f"<t:{unix_ts}:f>"),
|
||||||
|
("Long Date+Time", f"<t:{unix_ts}:F>"),
|
||||||
|
("Relative", f"<t:{unix_ts}:R>"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _load_friend_map() -> dict[int, tuple[str, float]]:
|
||||||
|
"""Scan environment for ``TIMESTAMP_FRIEND_{ID}=Country`` entries.
|
||||||
|
|
||||||
|
Returns ``{user_id: (country_name, utc_offset)}``.
|
||||||
|
"""
|
||||||
|
friends: dict[int, tuple[str, float]] = {}
|
||||||
|
for key, raw in os.environ.items():
|
||||||
|
if not key.startswith("TIMESTAMP_FRIEND_"):
|
||||||
|
continue
|
||||||
|
uid_str = key.removeprefix("TIMESTAMP_FRIEND_")
|
||||||
|
try:
|
||||||
|
uid = int(uid_str)
|
||||||
|
except ValueError:
|
||||||
|
log.warning("timestamp_friends: invalid user ID in env var %s", key)
|
||||||
|
continue
|
||||||
|
country = raw.strip()
|
||||||
|
offset = find_offset(country)
|
||||||
|
if offset is None:
|
||||||
|
log.warning(
|
||||||
|
"timestamp_friends: unknown country '%s' for user %d (env var %s)",
|
||||||
|
country, uid, key,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
friends[uid] = (country, offset)
|
||||||
|
return friends
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_pm_default(
|
||||||
|
dt: datetime, matched_text: str,
|
||||||
|
) -> datetime:
|
||||||
|
"""If *matched_text* has no morning indicator and the hour is 1–11,
|
||||||
|
bump by 12 h to treat the bare number as PM."""
|
||||||
|
if _MORNING_RE.search(matched_text):
|
||||||
|
return dt
|
||||||
|
if 1 <= dt.hour <= 11:
|
||||||
|
return dt + timedelta(hours=12)
|
||||||
|
return dt
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# COG
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
class TimestampFriends(commands.Cog):
|
||||||
|
"""Automatically convert time expressions when a friend says 'my time'."""
|
||||||
|
|
||||||
|
def __init__(self, bot: commands.Bot) -> None:
|
||||||
|
self.bot = bot
|
||||||
|
self.friends = _load_friend_map()
|
||||||
|
if not self.friends:
|
||||||
|
log.info("timestamp_friends: no friends configured.")
|
||||||
|
else:
|
||||||
|
log.info("timestamp_friends: loaded %d friend(s).", len(self.friends))
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_message(self, message: discord.Message) -> None:
|
||||||
|
# --- quick bail-outs ---
|
||||||
|
if message.author.bot:
|
||||||
|
return
|
||||||
|
if not HAS_DATEPARSER:
|
||||||
|
return
|
||||||
|
if message.author.id not in self.friends:
|
||||||
|
return
|
||||||
|
if not _TRIGGER_RE.search(message.content):
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- strip the trigger phrase ---
|
||||||
|
cleaned = _TRIGGER_RE.sub(" ", message.content).strip()
|
||||||
|
if not cleaned:
|
||||||
|
return # message was literally just "my time"
|
||||||
|
|
||||||
|
# --- parse time expressions ---
|
||||||
|
try:
|
||||||
|
raw_matches = _search_dates(
|
||||||
|
cleaned,
|
||||||
|
languages=["en"],
|
||||||
|
settings={
|
||||||
|
"PREFER_DATES_FROM": "future",
|
||||||
|
"PREFER_DAY_OF_MONTH": "first",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
log.debug("timestamp_friends: search_dates failed", exc_info=True)
|
||||||
|
return # quiet fail
|
||||||
|
if not raw_matches:
|
||||||
|
return # quiet fail — nothing parseable
|
||||||
|
|
||||||
|
country, offset = self.friends[message.author.id]
|
||||||
|
tz = timezone(timedelta(hours=offset))
|
||||||
|
|
||||||
|
# --- process each match ---
|
||||||
|
results: list[tuple[datetime, int]] = [] # (local_dt, unix_ts)
|
||||||
|
seen: set[int] = set() # dedupe by unix ts
|
||||||
|
|
||||||
|
for matched_text, dt_naive in raw_matches:
|
||||||
|
# PM default when no meridian given
|
||||||
|
dt_adjusted = _apply_pm_default(dt_naive, matched_text)
|
||||||
|
|
||||||
|
# attach friend's timezone, then convert to UTC
|
||||||
|
dt_aware = dt_adjusted.replace(tzinfo=tz)
|
||||||
|
unix_ts = int(dt_aware.astimezone(timezone.utc).timestamp())
|
||||||
|
|
||||||
|
if unix_ts not in seen:
|
||||||
|
seen.add(unix_ts)
|
||||||
|
results.append((dt_adjusted, unix_ts))
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
return # quiet fail
|
||||||
|
|
||||||
|
# --- build reply ---
|
||||||
|
lines: list[str] = []
|
||||||
|
if len(results) > 1:
|
||||||
|
lines.append(f"*{len(results)} times found*")
|
||||||
|
|
||||||
|
for local_dt, unix_ts in results:
|
||||||
|
if lines:
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"**{local_dt.strftime('%d %B at %H:%M')}**")
|
||||||
|
lines.append(f"Unix: `{unix_ts}`")
|
||||||
|
lines.append("")
|
||||||
|
for label, tag in _all_formats(unix_ts):
|
||||||
|
lines.append(f"`{tag}` ← {label}")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append("*Times display in each viewer's local timezone.*")
|
||||||
|
|
||||||
|
await message.reply("\n".join(lines), mention_author=False)
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# setup
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot) -> None:
|
||||||
|
await bot.add_cog(TimestampFriends(bot))
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
"""Country → UTC offset mapping for the calendar cog."""
|
"""Shared timezone data — country → UTC offset, fuzzy lookup, autocomplete.
|
||||||
|
######################################################################
|
||||||
|
Used by the calendar, /timestamp, and timestamp-friends cogs.
|
||||||
|
"""
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
@@ -11,7 +14,8 @@ COUNTRY_TZ: dict[str, float] = {
|
|||||||
"Spain": 2, "Sweden": 2,
|
"Spain": 2, "Sweden": 2,
|
||||||
"Norway": 2, "Denmark": 2,
|
"Norway": 2, "Denmark": 2,
|
||||||
"Switzerland": 2, "Austria": 2,
|
"Switzerland": 2, "Austria": 2,
|
||||||
"Czechia": 2, "Greece": 3,
|
"Czechia": 2, "Czech Republic": 2,
|
||||||
|
"Czech": 2, "Greece": 3,
|
||||||
"Finland": 3, "Romania": 3,
|
"Finland": 3, "Romania": 3,
|
||||||
"Ukraine": 3, "Portugal": 1,
|
"Ukraine": 3, "Portugal": 1,
|
||||||
"Ireland": 1, "Iceland": 0,
|
"Ireland": 1, "Iceland": 0,
|
||||||
@@ -48,6 +52,23 @@ COUNTRY_TZ: dict[str, float] = {
|
|||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
|
def find_offset(country: str) -> float | None:
|
||||||
|
"""Look up a country's UTC offset. Case-insensitive fuzzy match."""
|
||||||
|
c = country.strip().lower()
|
||||||
|
if not c:
|
||||||
|
return None
|
||||||
|
for name, offset in COUNTRY_TZ.items():
|
||||||
|
if c == name.lower():
|
||||||
|
return offset
|
||||||
|
for name, offset in COUNTRY_TZ.items():
|
||||||
|
if name.lower().startswith(c):
|
||||||
|
return offset
|
||||||
|
for name, offset in COUNTRY_TZ.items():
|
||||||
|
if c in name.lower():
|
||||||
|
return offset
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def country_autocomplete(
|
async def country_autocomplete(
|
||||||
_interaction,
|
_interaction,
|
||||||
current: str,
|
current: str,
|
||||||
@@ -9,7 +9,7 @@ DISCORD_WEBHOOK_URL="yo channel webhook here"
|
|||||||
TELEGRAM_GROUP_ID="yo telegram group id here"
|
TELEGRAM_GROUP_ID="yo telegram group id here"
|
||||||
|
|
||||||
#twitter things
|
#twitter things
|
||||||
TWITTER_RSS_URL="https://nitter.net/CalabiyauLeaks/rss"
|
TWITTER_RSS_URL="https://nitter.net/CalabiyauLeaks/rss"
|
||||||
TWITTER_RSS_FALLBACKS=
|
TWITTER_RSS_FALLBACKS=
|
||||||
TWITTER_DISCORD_WEBHOOK="yo discord twitter channel here"
|
TWITTER_DISCORD_WEBHOOK="yo discord twitter channel here"
|
||||||
TWITTER_DISCORD_WEBHOOK_MAO="yo discord twitter channel here for mao server"
|
TWITTER_DISCORD_WEBHOOK_MAO="yo discord twitter channel here for mao server"
|
||||||
@@ -34,4 +34,10 @@ MELLO=""
|
|||||||
FEROX=""
|
FEROX=""
|
||||||
|
|
||||||
#calendar special webhook
|
#calendar special webhook
|
||||||
CALENDAR_WEBHOOK_AGENT=webhook agent loll
|
CALENDAR_WEBHOOK_AGENT=webhook agent loll
|
||||||
|
|
||||||
|
timestamp friends — format: TIMESTAMP_FRIEND_{DISCORD_USER_ID}=Country
|
||||||
|
# Examples:
|
||||||
|
TIMESTAMP_FRIEND_123456789012345678=Turkey
|
||||||
|
TIMESTAMP_FRIEND_987654321098765432=Japan
|
||||||
|
TIMESTAMP_FRIEND_555555555555555555=US East
|
||||||
@@ -75,6 +75,8 @@ async def main():
|
|||||||
await bot.load_extension('blue_archive.inventory')
|
await bot.load_extension('blue_archive.inventory')
|
||||||
await bot.load_extension('firebase_website')
|
await bot.load_extension('firebase_website')
|
||||||
await bot.load_extension('cogs.calendar')
|
await bot.load_extension('cogs.calendar')
|
||||||
|
await bot.load_extension('cogs.timestamp')
|
||||||
|
await bot.load_extension('cogs.timestamp_friends')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await bot.start(os.getenv('DISCORD_TOKEN'))
|
await bot.start(os.getenv('DISCORD_TOKEN'))
|
||||||
|
|||||||
Reference in new issue
Block a user