diff --git a/data/timezones.json b/data/timezones.json index d1a6c2de..3ced8d7b 100644 --- a/data/timezones.json +++ b/data/timezones.json @@ -1,3 +1,15 @@ { - "366895110027214858": "EST" + "version": 1, + "updatedAt": "2026-01-17T01:18:58.565Z", + "zones": { + "global:366895110027214858": { + "version": 1, + "userId": "366895110027214858", + "guildId": null, + "timezone": "America/New_York", + "label": "ET", + "createdAt": "2026-01-17T01:18:58.565Z", + "updatedAt": "2026-01-17T01:18:58.565Z" + } + } } diff --git a/src/commands/timezone/timezone.ts b/src/commands/timezone/timezone.ts index a0463d19..5ea9cfe4 100644 --- a/src/commands/timezone/timezone.ts +++ b/src/commands/timezone/timezone.ts @@ -16,11 +16,13 @@ export const data = new SlashCommandBuilder() .addSubcommand((s) => s .setName("set") - .setDescription("Save your timezone (IANA like America/New_York or alias like EST)") + .setDescription( + "Save your timezone (IANA like America/New_York or short name like ET)", + ) .addStringOption((o) => o .setName("zone") - .setDescription("IANA zone (America/New_York) or alias (EST, CST, PST)") + .setDescription('Examples: "US Eastern", "ET", "America/New_York"') .setRequired(true), ) .addBooleanOption((o) => @@ -87,7 +89,9 @@ export const data = new SlashCommandBuilder() .addStringOption((o) => o .setName("to") - .setDescription("Target zone (IANA or alias like PST)") + .setDescription( + 'Target zone (examples: "US Pacific", "PT", "America/Los_Angeles")', + ) .setRequired(true), ) .addStringOption((o) => @@ -108,7 +112,6 @@ export const data = new SlashCommandBuilder() export async function execute(interaction: ChatInputCommandInteraction): Promise { const sub = interaction.options.getSubcommand(true); - // Most timezone ops should be ephemeral to avoid channel noise. await interaction.deferReply({ ephemeral: true }); try { @@ -126,20 +129,54 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise } /* -------------------------------------------------------------------------- */ -/* IMPLEMENTATION */ +/* Timezone input normalization */ /* -------------------------------------------------------------------------- */ -const ALIAS_TO_IANA: Record = { - est: { tz: "America/New_York", label: "EST" }, - edt: { tz: "America/New_York", label: "EDT" }, - cst: { tz: "America/Chicago", label: "CST" }, - cdt: { tz: "America/Chicago", label: "CDT" }, - mst: { tz: "America/Denver", label: "MST" }, - mdt: { tz: "America/Denver", label: "MDT" }, - pst: { tz: "America/Los_Angeles", label: "PST" }, - pdt: { tz: "America/Los_Angeles", label: "PDT" }, - gmt: { tz: "Etc/UTC", label: "GMT" }, +/** + * Small curated set of friendly names -> IANA zones. + * This avoids exposing the full IANA list while covering common needs. + */ +const COMMON_TIMEZONES: Array<{ name: string; tz: string; label?: string }> = [ + { name: "US Eastern", tz: "America/New_York", label: "ET" }, + { name: "US Central", tz: "America/Chicago", label: "CT" }, + { name: "US Mountain", tz: "America/Denver", label: "MT" }, + { name: "US Pacific", tz: "America/Los_Angeles", label: "PT" }, + + { name: "UTC", tz: "Etc/UTC", label: "UTC" }, + + { name: "UK", tz: "Europe/London" }, + { name: "Central Europe", tz: "Europe/Berlin" }, + + { name: "India", tz: "Asia/Kolkata" }, + { name: "Japan", tz: "Asia/Tokyo" }, + { name: "Australia East", tz: "Australia/Sydney" }, +]; + +/** + * Common abbreviations people actually type. + * Note: abbreviations are ambiguous globally, but this is a pragmatic bot UX choice. + */ +const ALIAS_TO_IANA: Record = { + // US / common + et: { tz: "America/New_York", label: "ET" }, + est: { tz: "America/New_York", label: "ET" }, + edt: { tz: "America/New_York", label: "ET" }, + + ct: { tz: "America/Chicago", label: "CT" }, + cst: { tz: "America/Chicago", label: "CT" }, + cdt: { tz: "America/Chicago", label: "CT" }, + + mt: { tz: "America/Denver", label: "MT" }, + mst: { tz: "America/Denver", label: "MT" }, + mdt: { tz: "America/Denver", label: "MT" }, + + pt: { tz: "America/Los_Angeles", label: "PT" }, + pst: { tz: "America/Los_Angeles", label: "PT" }, + pdt: { tz: "America/Los_Angeles", label: "PT" }, + + // UTC-ish utc: { tz: "Etc/UTC", label: "UTC" }, + gmt: { tz: "Etc/UTC", label: "UTC" }, }; function isValidIanaZone(tz: string): boolean { @@ -151,14 +188,34 @@ function isValidIanaZone(tz: string): boolean { } } +function normalizeKey(s: string): string { + return s.trim().toLowerCase().replace(/\s+/g, " "); +} + function normalizeZoneInput(raw: string): { tz: string; label?: string } | null { const v = raw.trim(); if (!v) return null; - const key = v.toLowerCase(); - const hit = ALIAS_TO_IANA[key]; - if (hit) return { tz: hit.tz, label: hit.label }; + const key = normalizeKey(v); + + // Friendly names: "us eastern", "central europe", etc. + for (const z of COMMON_TIMEZONES) { + if (normalizeKey(z.name) === key) return { tz: z.tz, label: z.label }; + } + + // Allow a couple shorthand friendly variants people type + if (key === "eastern" || key === "east") return { tz: "America/New_York", label: "ET" }; + if (key === "central" || key === "midwest") + return { tz: "America/Chicago", label: "CT" }; + if (key === "mountain") return { tz: "America/Denver", label: "MT" }; + if (key === "pacific" || key === "west") + return { tz: "America/Los_Angeles", label: "PT" }; + // Abbreviations: "ET", "PST", etc. + const alias = ALIAS_TO_IANA[key.replace(/\./g, "")]; + if (alias) return { tz: alias.tz, label: alias.label }; + + // Power user path: accept IANA directly if (isValidIanaZone(v)) return { tz: v }; return null; @@ -227,6 +284,21 @@ async function getTzOrNull( return getUserTimezone({ userId, guildId, scope }); } +function shortHint(): string { + return [ + "Examples:", + '`/timezone set zone:"US Eastern"`', + "`/timezone set zone:ET`", + "`/timezone set zone:America/New_York`", + "", + "Common zones: US Eastern, US Central, US Mountain, US Pacific, UTC, UK, Central Europe, India, Japan, Australia East", + ].join("\n"); +} + +/* -------------------------------------------------------------------------- */ +/* Handlers */ +/* -------------------------------------------------------------------------- */ + async function handleSet(interaction: ChatInputCommandInteraction): Promise { const raw = interaction.options.getString("zone", true); const guildFlag = interaction.options.getBoolean("guild") ?? false; @@ -235,7 +307,7 @@ async function handleSet(interaction: ChatInputCommandInteraction): Promise