HASH 167fbb4dc56c
DATE 2025-12-06
SUBJECT discord: improve autocomplete, class color embed border
FILES 6 CHANGED
HASH 167fbb4dc56c
DATE 2025-12-06
SUBJECT discord: improve autocomplete, class
color embed border
FILES 6 CHANGED
┌─ web/api/discord.ts ───────────────────────────────────────────────────────┐
│ diff --git a/web/api/discord.ts b/web/api/discord.ts │
│ index db1b519..db6037b 100644 │
│ --- a/web/api/discord.ts │
│ +++ b/web/api/discord.ts │
│ @@ -15,6 +15,7 @@ import { handlePlayerCommand } from "../src/discord/commands/pla │
│ yer.js"; │
│ import { handleLeaderboardCommand } from "../src/discord/commands/leaderboard.js" │
│ ; │
│ import { autocompleteDungeon } from "../src/discord/autocomplete/dungeons.js"; │
│ import { autocompleteRealm } from "../src/discord/autocomplete/realms.js"; │
│ +import { autocompleteRegionByPlayer } from "../src/discord/autocomplete/regions.j │
│ s"; │
│ import { autocompletePlayer } from "../src/discord/autocomplete/players.js"; │
│ import { autocompleteClass } from "../src/discord/autocomplete/classes.js"; │
│ import { createGenericErrorEmbed } from "../src/discord/embeds/error.js"; │
│ @@ -22,6 +23,10 @@ import { createGenericErrorEmbed } from "../src/discord/embeds/ │
│ error.js"; │
│ // cache initialization flag │
│ let cacheInitialized = false; │
│ │
│ +// refresh cooldown tracking (message_id -> timestamp) │
│ +const refreshCooldowns = new Map<string, number>(); │
│ +const REFRESH_COOLDOWN_MS = 30000; // 30 seconds │
│ + │
│ /** │
│ * Main Discord interaction handler │
│ */ │
│ @@ -147,7 +152,10 @@ async function handleAutocomplete(interaction: DiscordInterac │
│ tion) { │
│ if (focusedOption.name === "realm") { │
│ const region = getOptionValue(options[0]?.options || [], "region"); │
│ const regionStr = typeof region === "string" ? region : undefined; │
│ - return autocompleteRealm(query, regionStr as "us" | "eu" | "kr" | "tw" | un │
│ defined); │
│ + return autocompleteRealm( │
│ + query, │
│ + regionStr as "us" | "eu" | "kr" | "tw" | undefined, │
│ + ); │
│ } │
│ if (focusedOption.name === "class") { │
│ return autocompleteClass(query); │
│ @@ -164,10 +172,34 @@ async function handleAutocomplete(interaction: DiscordIntera │
│ ction) { │
│ typeof realm === "string" ? realm : undefined, │
│ ); │
│ } │
│ + if (focusedOption.name === "region") { │
│ + const name = getOptionValue(options, "name"); │
│ + console.log("[Discord Handler] All options:", JSON.stringify(options)); │
│ + console.log( │
│ + "[Discord Handler] Region autocomplete - name option value:", │
│ + name, │
│ + "type:", │
│ + typeof name, │
│ + ); │
│ + console.log( │
│ + "[Discord Handler] Focused option:", │
│ + JSON.stringify(focusedOption), │
│ + ); │
│ + return autocompleteRegionByPlayer( │
│ + query, │
│ + typeof name === "string" ? name : undefined, │
│ + ); │
│ + } │
│ if (focusedOption.name === "realm") { │
│ const region = getOptionValue(options, "region"); │
│ + const name = getOptionValue(options, "name"); │
│ const regionStr = typeof region === "string" ? region : undefined; │
│ - return autocompleteRealm(query, regionStr as "us" | "eu" | "kr" | "tw" | un │
│ defined); │
│ + const nameStr = typeof name === "string" ? name : undefined; │
│ + return autocompleteRealm( │
│ + query, │
│ + regionStr as "us" | "eu" | "kr" | "tw" | undefined, │
│ + nameStr, │
│ + ); │
│ } │
│ } │
│ │
│ @@ -199,11 +231,38 @@ async function handleCommand(interaction: DiscordInteraction │
│ ) { │
│ async function handleButton(interaction: DiscordInteraction) { │
│ const customId = interaction.data?.custom_id || ""; │
│ │
│ + // check cooldown for all refresh actions │
│ + if (customId.startsWith("refresh_")) { │
│ + const messageId = interaction.message?.id; │
│ + if (messageId) { │
│ + const lastRefresh = refreshCooldowns.get(messageId); │
│ + const now = Date.now(); │
│ + │
│ + if (lastRefresh && now - lastRefresh < REFRESH_COOLDOWN_MS) { │
│ + // silently ignore refresh attempts within cooldown │
│ + // return current message unchanged │
│ + return interaction.message || {}; │
│ + } │
│ + │
│ + // update cooldown timestamp │
│ + refreshCooldowns.set(messageId, now); │
│ + │
│ + // cleanup old entries (older than 5 minutes) │
│ + for (const [id, timestamp] of refreshCooldowns.entries()) { │
│ + if (now - timestamp > 300000) { │
│ + refreshCooldowns.delete(id); │
│ + } │
│ + } │
│ + } │
│ + } │
│ + │
│ // handle refresh player profile │
│ if (customId.startsWith("refresh_player:")) { │
│ const [, region, realm, name] = customId.split(":"); │
│ │
│ - const { handlePlayerCommand } = await import("../src/discord/commands/player. │
│ js"); │
│ + const { handlePlayerCommand } = await import( │
│ + "../src/discord/commands/player.js" │
│ + ); │
│ │
│ const fakeInteraction: DiscordInteraction = { │
│ ...interaction, │
│ @@ -223,9 +282,12 @@ async function handleButton(interaction: DiscordInteraction) │
│ { │
│ │
│ // handle refresh dungeon leaderboard │
│ if (customId.startsWith("refresh_dungeon:")) { │
│ - const [, dungeon, scope, region, realm, limit, season] = customId.split(":"); │
│ + const [, dungeon, scope, region, realm, limit, season] = │
│ + customId.split(":"); │
│ │
│ - const { handleLeaderboardCommand } = await import("../src/discord/commands/le │
│ aderboard.js"); │
│ + const { handleLeaderboardCommand } = await import( │
│ + "../src/discord/commands/leaderboard.js" │
│ + ); │
│ │
│ const options: any[] = [ │
│ { name: "dungeon", type: 3, value: dungeon }, // STRING │
│ @@ -250,9 +312,12 @@ async function handleButton(interaction: DiscordInteraction) │
│ { │
│ │
│ // handle refresh player leaderboard │
│ if (customId.startsWith("refresh_players:")) { │
│ - const [, scope, region, realm, className, limit, season] = customId.split(":" │
│ ); │
│ + const [, scope, region, realm, className, limit, season] = │
│ + customId.split(":"); │
│ │
│ - const { handleLeaderboardCommand } = await import("../src/discord/commands/le │
│ aderboard.js"); │
│ + const { handleLeaderboardCommand } = await import( │
│ + "../src/discord/commands/leaderboard.js" │
│ + ); │
│ │
│ const options: any[] = [{ name: "scope", type: 3, value: scope }]; // STRING │
│ if (region) options.push({ name: "region", type: 3, value: region }); // STRI │
│ NG │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/api/discord.ts ─────────────────┐
│ diff --git a/web/api/discord.ts b/web/api/di │
│ scord.ts │
│ index db1b519..db6037b 100644 │
│ --- a/web/api/discord.ts │
│ +++ b/web/api/discord.ts │
│ @@ -15,6 +15,7 @@ import { handlePlayerComma │
│ nd } from "../src/discord/commands/player.js │
│ "; │
│ import { handleLeaderboardCommand } from ". │
│ ./src/discord/commands/leaderboard.js"; │
│ import { autocompleteDungeon } from "../src │
│ /discord/autocomplete/dungeons.js"; │
│ import { autocompleteRealm } from "../src/d │
│ iscord/autocomplete/realms.js"; │
│ +import { autocompleteRegionByPlayer } from │
│ "../src/discord/autocomplete/regions.js"; │
│ import { autocompletePlayer } from "../src/ │
│ discord/autocomplete/players.js"; │
│ import { autocompleteClass } from "../src/d │
│ iscord/autocomplete/classes.js"; │
│ import { createGenericErrorEmbed } from ".. │
│ /src/discord/embeds/error.js"; │
│ @@ -22,6 +23,10 @@ import { createGenericErr │
│ orEmbed } from "../src/discord/embeds/error. │
│ js"; │
│ // cache initialization flag │
│ let cacheInitialized = false; │
│ │
│ +// refresh cooldown tracking (message_id -> │
│ timestamp) │
│ +const refreshCooldowns = new Map<string, nu │
│ mber>(); │
│ +const REFRESH_COOLDOWN_MS = 30000; // 30 se │
│ conds │
│ + │
│ /** │
│ * Main Discord interaction handler │
│ */ │
│ @@ -147,7 +152,10 @@ async function handleAu │
│ tocomplete(interaction: DiscordInteraction) │
│ { │
│ if (focusedOption.name === "realm") { │
│ const region = getOptionValue(options │
│ [0]?.options || [], "region"); │
│ const regionStr = typeof region === " │
│ string" ? region : undefined; │
│ - return autocompleteRealm(query, regio │
│ nStr as "us" | "eu" | "kr" | "tw" | undefine │
│ d); │
│ + return autocompleteRealm( │
│ + query, │
│ + regionStr as "us" | "eu" | "kr" | " │
│ tw" | undefined, │
│ + ); │
│ } │
│ if (focusedOption.name === "class") { │
│ return autocompleteClass(query); │
│ @@ -164,10 +172,34 @@ async function handleA │
│ utocomplete(interaction: DiscordInteraction) │
│ { │
│ typeof realm === "string" ? realm : │
│ undefined, │
│ ); │
│ } │
│ + if (focusedOption.name === "region") { │
│ + const name = getOptionValue(options, │
│ "name"); │
│ + console.log("[Discord Handler] All op │
│ tions:", JSON.stringify(options)); │
│ + console.log( │
│ + "[Discord Handler] Region autocompl │
│ ete - name option value:", │
│ + name, │
│ + "type:", │
│ + typeof name, │
│ + ); │
│ + console.log( │
│ + "[Discord Handler] Focused option:" │
│ , │
│ + JSON.stringify(focusedOption), │
│ + ); │
│ + return autocompleteRegionByPlayer( │
│ + query, │
│ + typeof name === "string" ? name : u │
│ ndefined, │
│ + ); │
│ + } │
│ if (focusedOption.name === "realm") { │
│ const region = getOptionValue(options │
│ , "region"); │
│ + const name = getOptionValue(options, │
│ "name"); │
│ const regionStr = typeof region === " │
│ string" ? region : undefined; │
│ - return autocompleteRealm(query, regio │
│ nStr as "us" | "eu" | "kr" | "tw" | undefine │
│ d); │
│ + const nameStr = typeof name === "stri │
│ ng" ? name : undefined; │
│ + return autocompleteRealm( │
│ + query, │
│ + regionStr as "us" | "eu" | "kr" | " │
│ tw" | undefined, │
│ + nameStr, │
│ + ); │
│ } │
│ } │
│ │
│ @@ -199,11 +231,38 @@ async function handleC │
│ ommand(interaction: DiscordInteraction) { │
│ async function handleButton(interaction: Di │
│ scordInteraction) { │
│ const customId = interaction.data?.custom │
│ _id || ""; │
│ │
│ + // check cooldown for all refresh actions │
│ + if (customId.startsWith("refresh_")) { │
│ + const messageId = interaction.message?. │
│ id; │
│ + if (messageId) { │
│ + const lastRefresh = refreshCooldowns. │
│ get(messageId); │
│ + const now = Date.now(); │
│ + │
│ + if (lastRefresh && now - lastRefresh │
│ < REFRESH_COOLDOWN_MS) { │
│ + // silently ignore refresh attempts │
│ within cooldown │
│ + // return current message unchanged │
│ + return interaction.message || {}; │
│ + } │
│ + │
│ + // update cooldown timestamp │
│ + refreshCooldowns.set(messageId, now); │
│ + │
│ + // cleanup old entries (older than 5 │
│ minutes) │
│ + for (const [id, timestamp] of refresh │
│ Cooldowns.entries()) { │
│ + if (now - timestamp > 300000) { │
│ + refreshCooldowns.delete(id); │
│ + } │
│ + } │
│ + } │
│ + } │
│ + │
│ // handle refresh player profile │
│ if (customId.startsWith("refresh_player:" │
│ )) { │
│ const [, region, realm, name] = customI │
│ d.split(":"); │
│ │
│ - const { handlePlayerCommand } = await i │
│ mport("../src/discord/commands/player.js"); │
│ + const { handlePlayerCommand } = await i │
│ mport( │
│ + "../src/discord/commands/player.js" │
│ + ); │
│ │
│ const fakeInteraction: DiscordInteracti │
│ on = { │
│ ...interaction, │
│ @@ -223,9 +282,12 @@ async function handleBu │
│ tton(interaction: DiscordInteraction) { │
│ │
│ // handle refresh dungeon leaderboard │
│ if (customId.startsWith("refresh_dungeon: │
│ ")) { │
│ - const [, dungeon, scope, region, realm, │
│ limit, season] = customId.split(":"); │
│ + const [, dungeon, scope, region, realm, │
│ limit, season] = │
│ + customId.split(":"); │
│ │
│ - const { handleLeaderboardCommand } = aw │
│ ait import("../src/discord/commands/leaderbo │
│ ard.js"); │
│ + const { handleLeaderboardCommand } = aw │
│ ait import( │
│ + "../src/discord/commands/leaderboard. │
│ js" │
│ + ); │
│ │
│ const options: any[] = [ │
│ { name: "dungeon", type: 3, value: du │
│ ngeon }, // STRING │
│ @@ -250,9 +312,12 @@ async function handleBu │
│ tton(interaction: DiscordInteraction) { │
│ │
│ // handle refresh player leaderboard │
│ if (customId.startsWith("refresh_players: │
│ ")) { │
│ - const [, scope, region, realm, classNam │
│ e, limit, season] = customId.split(":"); │
│ + const [, scope, region, realm, classNam │
│ e, limit, season] = │
│ + customId.split(":"); │
│ │
│ - const { handleLeaderboardCommand } = aw │
│ ait import("../src/discord/commands/leaderbo │
│ ard.js"); │
│ + const { handleLeaderboardCommand } = aw │
│ ait import( │
│ + "../src/discord/commands/leaderboard. │
│ js" │
│ + ); │
│ │
│ const options: any[] = [{ name: "scope" │
│ , type: 3, value: scope }]; // STRING │
│ if (region) options.push({ name: "regio │
│ n", type: 3, value: region }); // STRING │
└──────────────────────────────────────────────┘
┌─ web/src/discord/autocomplete/realms.ts ───────────────────────────────────┐
│ diff --git a/web/src/discord/autocomplete/realms.ts b/web/src/discord/autocomplete │
│ /realms.ts │
│ index 9381824..69a3610 100644 │
│ --- a/web/src/discord/autocomplete/realms.ts │
│ +++ b/web/src/discord/autocomplete/realms.ts │
│ @@ -1,56 +1,87 @@ │
│ // Realm autocomplete handler │
│ │
│ -import { getRealms } from "../indexes/cache.js"; │
│ +import { getRealms, getPlayers } from "../indexes/cache.js"; │
│ import type { AutocompleteChoice } from "../types.js"; │
│ import { DISCORD_LIMITS } from "../constants.js"; │
│ │
│ /** │
│ * Handles autocomplete for realm selection │
│ * Filters by region and fuzzy matches on realm name │
│ + * Optionally filters by player name if provided │
│ */ │
│ export async function autocompleteRealm( │
│ - query: string, │
│ - region?: "us" | "eu" | "kr" | "tw", │
│ + query: string, │
│ + region?: "us" | "eu" | "kr" | "tw", │
│ + playerName?: string, │
│ ): Promise<AutocompleteChoice[]> { │
│ - // if no region specified, return empty (user must select region first) │
│ - if (!region) { │
│ - return [ │
│ - { │
│ - name: "Please select a region first", │
│ - value: "none", │
│ - }, │
│ - ]; │
│ - } │
│ - │
│ - const realms = await getRealms(region); │
│ - const lowerQuery = query.toLowerCase().trim(); │
│ - │
│ - // sort by player count (most popular first) │
│ - const sortedRealms = [...realms].sort( │
│ - (a, b) => b.player_count - a.player_count, │
│ - ); │
│ - │
│ - // if no query, return top realms │
│ - if (!lowerQuery) { │
│ - return sortedRealms │
│ - .slice(0, DISCORD_LIMITS.MAX_AUTOCOMPLETE_CHOICES) │
│ - .map((r) => ({ │
│ - name: `${r.name} (${r.player_count.toLocaleString()} players)`, │
│ - value: r.slug, │
│ - })); │
│ - } │
│ - │
│ - // filter realms by name or slug │
│ - const filtered = sortedRealms │
│ - .filter( │
│ - (r) => │
│ - r.name.toLowerCase().includes(lowerQuery) || │
│ - r.slug.toLowerCase().includes(lowerQuery), │
│ - ) │
│ - .slice(0, DISCORD_LIMITS.MAX_AUTOCOMPLETE_CHOICES); │
│ - │
│ - return filtered.map((r) => ({ │
│ - name: `${r.name} (${r.player_count.toLocaleString()} players)`, │
│ - value: r.slug, │
│ - })); │
│ + // if no region specified, return empty (user must select region first) │
│ + if (!region) { │
│ + return [ │
│ + { │
│ + name: "Please select a region first", │
│ + value: "none", │
│ + }, │
│ + ]; │
│ + } │
│ + │
│ + let realms = await getRealms(region); │
│ + const lowerQuery = query.toLowerCase().trim(); │
│ + │
│ + // if player name is provided, filter realms to only those where player exists │
│ + if (playerName && playerName.trim() !== "") { │
│ + const players = await getPlayers(); │
│ + const lowerPlayerName = playerName.toLowerCase().trim(); │
│ + │
│ + // find all players matching the name in this region │
│ + const matchingPlayers = players.filter( │
│ + (p) => │
│ + p.name.toLowerCase().includes(lowerPlayerName) && │
│ + p.region === region.toLowerCase(), │
│ + ); │
│ + │
│ + // extract unique realm slugs from matching players │
│ + const playerRealms = new Set(matchingPlayers.map((p) => p.realm_slug)); │
│ + │
│ + // filter realms to only those where the player exists │
│ + realms = realms.filter((r) => playerRealms.has(r.slug)); │
│ + │
│ + // if no realms found, show helpful message │
│ + if (realms.length === 0) { │
│ + return [ │
│ + { │
│ + name: `No players named "${playerName}" found in ${region.toUpperCase() │
│ }`, │
│ + value: "none", │
│ + }, │
│ + ]; │
│ + } │
│ + } │
│ + │
│ + // sort by player count (most popular first) │
│ + const sortedRealms = [...realms].sort( │
│ + (a, b) => b.player_count - a.player_count, │
│ + ); │
│ + │
│ + // if no query, return top realms │
│ + if (!lowerQuery) { │
│ + return sortedRealms │
│ + .slice(0, DISCORD_LIMITS.MAX_AUTOCOMPLETE_CHOICES) │
│ + .map((r) => ({ │
│ + name: r.name, │
│ + value: r.slug, │
│ + })); │
│ + } │
│ + │
│ + // filter realms by name or slug │
│ + const filtered = sortedRealms │
│ + .filter( │
│ + (r) => │
│ + r.name.toLowerCase().includes(lowerQuery) || │
│ + r.slug.toLowerCase().includes(lowerQuery), │
│ + ) │
│ + .slice(0, DISCORD_LIMITS.MAX_AUTOCOMPLETE_CHOICES); │
│ + │
│ + return filtered.map((r) => ({ │
│ + name: r.name, │
│ + value: r.slug, │
│ + })); │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...iscord/autocomplete/realms.ts ───┐
│ diff --git a/web/src/discord/autocomplete/re │
│ alms.ts b/web/src/discord/autocomplete/realm │
│ s.ts │
│ index 9381824..69a3610 100644 │
│ --- a/web/src/discord/autocomplete/realms.ts │
│ +++ b/web/src/discord/autocomplete/realms.ts │
│ @@ -1,56 +1,87 @@ │
│ // Realm autocomplete handler │
│ │
│ -import { getRealms } from "../indexes/cache │
│ .js"; │
│ +import { getRealms, getPlayers } from "../i │
│ ndexes/cache.js"; │
│ import type { AutocompleteChoice } from ".. │
│ /types.js"; │
│ import { DISCORD_LIMITS } from "../constant │
│ s.js"; │
│ │
│ /** │
│ * Handles autocomplete for realm selection │
│ * Filters by region and fuzzy matches on r │
│ ealm name │
│ + * Optionally filters by player name if pro │
│ vided │
│ */ │
│ export async function autocompleteRealm( │
│ - query: string, │
│ - region?: "us" | "eu" | "kr" | "tw", │
│ + query: string, │
│ + region?: "us" | "eu" | "kr" | "tw", │
│ + playerName?: string, │
│ ): Promise<AutocompleteChoice[]> { │
│ - // if no region specified, return empty (u │
│ ser must select region first) │
│ - if (!region) { │
│ - return [ │
│ - { │
│ - name: "Please select a region first", │
│ - value: "none", │
│ - }, │
│ - ]; │
│ - } │
│ - │
│ - const realms = await getRealms(region); │
│ - const lowerQuery = query.toLowerCase().tri │
│ m(); │
│ - │
│ - // sort by player count (most popular firs │
│ t) │
│ - const sortedRealms = [...realms].sort( │
│ - (a, b) => b.player_count - a.player_count │
│ , │
│ - ); │
│ - │
│ - // if no query, return top realms │
│ - if (!lowerQuery) { │
│ - return sortedRealms │
│ - .slice(0, DISCORD_LIMITS.MAX_AUTOCOMPLET │
│ E_CHOICES) │
│ - .map((r) => ({ │
│ - name: `${r.name} (${r.player_count.toLo │
│ caleString()} players)`, │
│ - value: r.slug, │
│ - })); │
│ - } │
│ - │
│ - // filter realms by name or slug │
│ - const filtered = sortedRealms │
│ - .filter( │
│ - (r) => │
│ - r.name.toLowerCase().includes(lowerQuer │
│ y) || │
│ - r.slug.toLowerCase().includes(lowerQuer │
│ y), │
│ - ) │
│ - .slice(0, DISCORD_LIMITS.MAX_AUTOCOMPLETE │
│ _CHOICES); │
│ - │
│ - return filtered.map((r) => ({ │
│ - name: `${r.name} (${r.player_count.toLoca │
│ leString()} players)`, │
│ - value: r.slug, │
│ - })); │
│ + // if no region specified, return empty ( │
│ user must select region first) │
│ + if (!region) { │
│ + return [ │
│ + { │
│ + name: "Please select a region first │
│ ", │
│ + value: "none", │
│ + }, │
│ + ]; │
│ + } │
│ + │
│ + let realms = await getRealms(region); │
│ + const lowerQuery = query.toLowerCase().tr │
│ im(); │
│ + │
│ + // if player name is provided, filter rea │
│ lms to only those where player exists │
│ + if (playerName && playerName.trim() !== " │
│ ") { │
│ + const players = await getPlayers(); │
│ + const lowerPlayerName = playerName.toLo │
│ werCase().trim(); │
│ + │
│ + // find all players matching the name i │
│ n this region │
│ + const matchingPlayers = players.filter( │
│ + (p) => │
│ + p.name.toLowerCase().includes(lower │
│ PlayerName) && │
│ + p.region === region.toLowerCase(), │
│ + ); │
│ + │
│ + // extract unique realm slugs from matc │
│ hing players │
│ + const playerRealms = new Set(matchingPl │
│ ayers.map((p) => p.realm_slug)); │
│ + │
│ + // filter realms to only those where th │
│ e player exists │
│ + realms = realms.filter((r) => playerRea │
│ lms.has(r.slug)); │
│ + │
│ + // if no realms found, show helpful mes │
│ sage │
│ + if (realms.length === 0) { │
│ + return [ │
│ + { │
│ + name: `No players named "${player │
│ Name}" found in ${region.toUpperCase()}`, │
│ + value: "none", │
│ + }, │
│ + ]; │
│ + } │
│ + } │
│ + │
│ + // sort by player count (most popular fir │
│ st) │
│ + const sortedRealms = [...realms].sort( │
│ + (a, b) => b.player_count - a.player_cou │
│ nt, │
│ + ); │
│ + │
│ + // if no query, return top realms │
│ + if (!lowerQuery) { │
│ + return sortedRealms │
│ + .slice(0, DISCORD_LIMITS.MAX_AUTOCOMP │
│ LETE_CHOICES) │
│ + .map((r) => ({ │
│ + name: r.name, │
│ + value: r.slug, │
│ + })); │
│ + } │
│ + │
│ + // filter realms by name or slug │
│ + const filtered = sortedRealms │
│ + .filter( │
│ + (r) => │
│ + r.name.toLowerCase().includes(lower │
│ Query) || │
│ + r.slug.toLowerCase().includes(lower │
│ Query), │
│ + ) │
│ + .slice(0, DISCORD_LIMITS.MAX_AUTOCOMPLE │
│ TE_CHOICES); │
│ + │
│ + return filtered.map((r) => ({ │
│ + name: r.name, │
│ + value: r.slug, │
│ + })); │
│ } │
└──────────────────────────────────────────────┘
┌─ web/src/discord/autocomplete/regions.ts ──────────────────────────────────┐
│ diff --git a/web/src/discord/autocomplete/regions.ts b/web/src/discord/autocomplet │
│ e/regions.ts │
│ new file mode 100644 │
│ index 0000000..fbb1053 │
│ --- /dev/null │
│ +++ b/web/src/discord/autocomplete/regions.ts │
│ @@ -0,0 +1,77 @@ │
│ +// region autocomplete handler │
│ + │
│ +import { getPlayers } from "../indexes/cache.js"; │
│ +import type { AutocompleteChoice } from "../types.js"; │
│ +import { DISCORD_LIMITS } from "../constants.js"; │
│ + │
│ +/** │
│ + * handles autocomplete for region selection based on player name │
│ + * filters regions to only show those where the specified player exists │
│ + */ │
│ +export async function autocompleteRegionByPlayer( │
│ + query: string, │
│ + playerName?: string, │
│ +): Promise<AutocompleteChoice[]> { │
│ + // Add debug logging │
│ + console.log("[Region Autocomplete] playerName:", playerName, "query:", query); │
│ + │
│ + // if no player name provided, return all regions │
│ + if (!playerName || playerName.trim() === "") { │
│ + console.log("[Region Autocomplete] No player name, returning all regions"); │
│ + return [ │
│ + { name: "US", value: "us" }, │
│ + { name: "EU", value: "eu" }, │
│ + { name: "KR", value: "kr" }, │
│ + { name: "TW", value: "tw" }, │
│ + ]; │
│ + } │
│ + │
│ + const players = await getPlayers(); │
│ + const lowerPlayerName = playerName.toLowerCase().trim(); │
│ + │
│ + // find all players matching the name (exact match first, then partial) │
│ + let matchingPlayers = players.filter( │
│ + (p) => p.name.toLowerCase() === lowerPlayerName, │
│ + ); │
│ + │
│ + // if no exact matches, try partial match │
│ + if (matchingPlayers.length === 0) { │
│ + matchingPlayers = players.filter((p) => │
│ + p.name.toLowerCase().includes(lowerPlayerName), │
│ + ); │
│ + } │
│ + │
│ + // extract unique regions from matching players │
│ + const regions = new Set<string>(); │
│ + for (const player of matchingPlayers) { │
│ + regions.add(player.region); │
│ + } │
│ + │
│ + // convert to autocomplete choices │
│ + const regionMap: Record<string, string> = { │
│ + us: "US", │
│ + eu: "EU", │
│ + kr: "KR", │
│ + tw: "TW", │
│ + }; │
│ + │
│ + const choices: AutocompleteChoice[] = []; │
│ + for (const region of Array.from(regions).sort()) { │
│ + choices.push({ │
│ + name: regionMap[region] || region.toUpperCase(), │
│ + value: region, │
│ + }); │
│ + } │
│ + │
│ + // if no regions found, show helpful message │
│ + if (choices.length === 0) { │
│ + return [ │
│ + { │
│ + name: `No players found matching "${playerName}"`, │
│ + value: "none", │
│ + }, │
│ + ]; │
│ + } │
│ + │
│ + return choices; │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...scord/autocomplete/regions.ts ───┐
│ diff --git a/web/src/discord/autocomplete/re │
│ gions.ts b/web/src/discord/autocomplete/regi │
│ ons.ts │
│ new file mode 100644 │
│ index 0000000..fbb1053 │
│ --- /dev/null │
│ +++ b/web/src/discord/autocomplete/regions.t │
│ s │
│ @@ -0,0 +1,77 @@ │
│ +// region autocomplete handler │
│ + │
│ +import { getPlayers } from "../indexes/cach │
│ e.js"; │
│ +import type { AutocompleteChoice } from ".. │
│ /types.js"; │
│ +import { DISCORD_LIMITS } from "../constant │
│ s.js"; │
│ + │
│ +/** │
│ + * handles autocomplete for region selectio │
│ n based on player name │
│ + * filters regions to only show those where │
│ the specified player exists │
│ + */ │
│ +export async function autocompleteRegionByP │
│ layer( │
│ + query: string, │
│ + playerName?: string, │
│ +): Promise<AutocompleteChoice[]> { │
│ + // Add debug logging │
│ + console.log("[Region Autocomplete] player │
│ Name:", playerName, "query:", query); │
│ + │
│ + // if no player name provided, return all │
│ regions │
│ + if (!playerName || playerName.trim() === │
│ "") { │
│ + console.log("[Region Autocomplete] No p │
│ layer name, returning all regions"); │
│ + return [ │
│ + { name: "US", value: "us" }, │
│ + { name: "EU", value: "eu" }, │
│ + { name: "KR", value: "kr" }, │
│ + { name: "TW", value: "tw" }, │
│ + ]; │
│ + } │
│ + │
│ + const players = await getPlayers(); │
│ + const lowerPlayerName = playerName.toLowe │
│ rCase().trim(); │
│ + │
│ + // find all players matching the name (ex │
│ act match first, then partial) │
│ + let matchingPlayers = players.filter( │
│ + (p) => p.name.toLowerCase() === lowerPl │
│ ayerName, │
│ + ); │
│ + │
│ + // if no exact matches, try partial match │
│ + if (matchingPlayers.length === 0) { │
│ + matchingPlayers = players.filter((p) => │
│ + p.name.toLowerCase().includes(lowerPl │
│ ayerName), │
│ + ); │
│ + } │
│ + │
│ + // extract unique regions from matching p │
│ layers │
│ + const regions = new Set<string>(); │
│ + for (const player of matchingPlayers) { │
│ + regions.add(player.region); │
│ + } │
│ + │
│ + // convert to autocomplete choices │
│ + const regionMap: Record<string, string> = │
│ { │
│ + us: "US", │
│ + eu: "EU", │
│ + kr: "KR", │
│ + tw: "TW", │
│ + }; │
│ + │
│ + const choices: AutocompleteChoice[] = []; │
│ + for (const region of Array.from(regions). │
│ sort()) { │
│ + choices.push({ │
│ + name: regionMap[region] || region.toU │
│ pperCase(), │
│ + value: region, │
│ + }); │
│ + } │
│ + │
│ + // if no regions found, show helpful mess │
│ age │
│ + if (choices.length === 0) { │
│ + return [ │
│ + { │
│ + name: `No players found matching "$ │
│ {playerName}"`, │
│ + value: "none", │
│ + }, │
│ + ]; │
│ + } │
│ + │
│ + return choices; │
│ +} │
└──────────────────────────────────────────────┘
┌─ web/src/discord/constants.ts ─────────────────────────────────────────────┐
│ diff --git a/web/src/discord/constants.ts b/web/src/discord/constants.ts │
│ index 9cbc2af..60a9e7c 100644 │
│ --- a/web/src/discord/constants.ts │
│ +++ b/web/src/discord/constants.ts │
│ @@ -1,16 +1,16 @@ │
│ -// colors for embeds (hex converted to decimal) │
│ -export const COLORS = { │
│ - ARTIFACT: 0xe6cc80, // gold/legendary │
│ - EXCELLENT: 0xa335ee, // purple/epic │
│ - EPIC: 0xa335ee, // purple/epic │
│ - RARE: 0x0070dd, // blue/rare │
│ - UNCOMMON: 0x1eff00, // green/uncommon │
│ - COMMON: 0x9d9d9d, // gray/common │
│ - DEFAULT: 0x5865f2, // discord blurple │
│ - SUCCESS: 0x57f287, // green │
│ - WARNING: 0xfee75c, // yellow │
│ - ERROR: 0xed4245, // red │
│ - INFO: 0x3498db, // blue │
│ +// class colors for embeds (hex converted to decimal) │
│ +export const CLASS_COLORS = { │
│ + death_knight: 0xc41e3a, │
│ + druid: 0xff7c0a, │
│ + hunter: 0xaad372, │
│ + mage: 0x3fc7eb, │
│ + monk: 0x00ff98, │
│ + paladin: 0xf48cba, │
│ + priest: 0xffffff, │
│ + rogue: 0xfff468, │
│ + shaman: 0x0070dd, │
│ + warlock: 0x8788ee, │
│ + warrior: 0xc69b6d, │
│ } as const; │
│ │
│ // discord custom emoji IDs for specs │
│ @@ -77,16 +77,6 @@ export const DUNGEON_EMOJI_IDS: Record<string, string> = { │
│ "temple-of-the-jade-serpent": "1446337487612936332", │
│ }; │
│ │
│ -// ranking bracket colors (match website colors) │
│ -export const BRACKET_COLORS = { │
│ - artifact: COLORS.ARTIFACT, │
│ - excellent: COLORS.EXCELLENT, │
│ - epic: COLORS.EPIC, │
│ - rare: COLORS.RARE, │
│ - uncommon: COLORS.UNCOMMON, │
│ - common: COLORS.COMMON, │
│ -} as const; │
│ - │
│ // ranking bracket display names │
│ export const BRACKET_NAMES = { │
│ artifact: "Artifact", │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/src/discord/constants.ts ───────┐
│ diff --git a/web/src/discord/constants.ts b/ │
│ web/src/discord/constants.ts │
│ index 9cbc2af..60a9e7c 100644 │
│ --- a/web/src/discord/constants.ts │
│ +++ b/web/src/discord/constants.ts │
│ @@ -1,16 +1,16 @@ │
│ -// colors for embeds (hex converted to deci │
│ mal) │
│ -export const COLORS = { │
│ - ARTIFACT: 0xe6cc80, // gold/legendary │
│ - EXCELLENT: 0xa335ee, // purple/epic │
│ - EPIC: 0xa335ee, // purple/epic │
│ - RARE: 0x0070dd, // blue/rare │
│ - UNCOMMON: 0x1eff00, // green/uncommon │
│ - COMMON: 0x9d9d9d, // gray/common │
│ - DEFAULT: 0x5865f2, // discord blurple │
│ - SUCCESS: 0x57f287, // green │
│ - WARNING: 0xfee75c, // yellow │
│ - ERROR: 0xed4245, // red │
│ - INFO: 0x3498db, // blue │
│ +// class colors for embeds (hex converted t │
│ o decimal) │
│ +export const CLASS_COLORS = { │
│ + death_knight: 0xc41e3a, │
│ + druid: 0xff7c0a, │
│ + hunter: 0xaad372, │
│ + mage: 0x3fc7eb, │
│ + monk: 0x00ff98, │
│ + paladin: 0xf48cba, │
│ + priest: 0xffffff, │
│ + rogue: 0xfff468, │
│ + shaman: 0x0070dd, │
│ + warlock: 0x8788ee, │
│ + warrior: 0xc69b6d, │
│ } as const; │
│ │
│ // discord custom emoji IDs for specs │
│ @@ -77,16 +77,6 @@ export const DUNGEON_EMOJ │
│ I_IDS: Record<string, string> = { │
│ "temple-of-the-jade-serpent": "1446337487 │
│ 612936332", │
│ }; │
│ │
│ -// ranking bracket colors (match website co │
│ lors) │
│ -export const BRACKET_COLORS = { │
│ - artifact: COLORS.ARTIFACT, │
│ - excellent: COLORS.EXCELLENT, │
│ - epic: COLORS.EPIC, │
│ - rare: COLORS.RARE, │
│ - uncommon: COLORS.UNCOMMON, │
│ - common: COLORS.COMMON, │
│ -} as const; │
│ - │
│ // ranking bracket display names │
│ export const BRACKET_NAMES = { │
│ artifact: "Artifact", │
└──────────────────────────────────────────────┘
┌─ web/src/discord/embeds/player-profile.ts ─────────────────────────────────┐
│ diff --git a/web/src/discord/embeds/player-profile.ts b/web/src/discord/embeds/pla │
│ yer-profile.ts │
│ index a86be52..9cdd9b9 100644 │
│ --- a/web/src/discord/embeds/player-profile.ts │
│ +++ b/web/src/discord/embeds/player-profile.ts │
│ @@ -7,6 +7,7 @@ import { │
│ SPEC_EMOJI_IDS, │
│ API_BASE_URL, │
│ DUNGEON_EMOJI_IDS, │
│ + CLASS_COLORS, │
│ } from "../constants.js"; │
│ import { formatDurationFromMs } from "../../lib/utils.js"; │
│ │
│ @@ -68,15 +69,18 @@ export function createPlayerProfileEmbed(profile: PlayerProfil │
│ eData): Embed { │
│ │
│ if (seasonData.global_ranking) { │
│ // only show percentile if not rank 1 (would be redundant) │
│ - const percentileText = seasonData.global_ranking === 1 ? "" : ` (${globalPe │
│ rcentile})`; │
│ + const percentileText = │
│ + seasonData.global_ranking === 1 ? "" : ` (${globalPercentile})`; │
│ rankingsText += `**Global:** #${seasonData.global_ranking}${percentileText} │
│ \n`; │
│ } │
│ if (seasonData.regional_ranking) { │
│ - const percentileText = seasonData.regional_ranking === 1 ? "" : ` (${region │
│ alPercentile})`; │
│ + const percentileText = │
│ + seasonData.regional_ranking === 1 ? "" : ` (${regionalPercentile})`; │
│ rankingsText += `**Regional:** #${seasonData.regional_ranking}${percentileT │
│ ext}\n`; │
│ } │
│ if (seasonData.realm_ranking) { │
│ - const percentileText = seasonData.realm_ranking === 1 ? "" : ` (${realmPerc │
│ entile})`; │
│ + const percentileText = │
│ + seasonData.realm_ranking === 1 ? "" : ` (${realmPercentile})`; │
│ rankingsText += `**Realm:** #${seasonData.realm_ranking}${percentileText}`; │
│ } │
│ │
│ @@ -113,10 +117,17 @@ export function createPlayerProfileEmbed(profile: PlayerProf │
│ ileData): Embed { │
│ } │
│ } │
│ │
│ + // get class color for embed sidebar │
│ + const classKey = player.class_name │
│ + ?.toLowerCase() │
│ + .replace(/\s+/g, "_") as keyof typeof CLASS_COLORS; │
│ + const color = CLASS_COLORS[classKey] || 0x5865f2; // fallback to discord blurpl │
│ e │
│ + │
│ return { │
│ title, │
│ description, │
│ fields, │
│ + color, │
│ footer: { │
│ text: "wowsimstats.com", │
│ }, │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...cord/embeds/player-profile.ts ───┐
│ diff --git a/web/src/discord/embeds/player-p │
│ rofile.ts b/web/src/discord/embeds/player-pr │
│ ofile.ts │
│ index a86be52..9cdd9b9 100644 │
│ --- a/web/src/discord/embeds/player-profile. │
│ ts │
│ +++ b/web/src/discord/embeds/player-profile. │
│ ts │
│ @@ -7,6 +7,7 @@ import { │
│ SPEC_EMOJI_IDS, │
│ API_BASE_URL, │
│ DUNGEON_EMOJI_IDS, │
│ + CLASS_COLORS, │
│ } from "../constants.js"; │
│ import { formatDurationFromMs } from "../.. │
│ /lib/utils.js"; │
│ │
│ @@ -68,15 +69,18 @@ export function createPl │
│ ayerProfileEmbed(profile: PlayerProfileData) │
│ : Embed { │
│ │
│ if (seasonData.global_ranking) { │
│ // only show percentile if not rank 1 │
│ (would be redundant) │
│ - const percentileText = seasonData.glo │
│ bal_ranking === 1 ? "" : ` (${globalPercenti │
│ le})`; │
│ + const percentileText = │
│ + seasonData.global_ranking === 1 ? " │
│ " : ` (${globalPercentile})`; │
│ rankingsText += `**Global:** #${seaso │
│ nData.global_ranking}${percentileText}\n`; │
│ } │
│ if (seasonData.regional_ranking) { │
│ - const percentileText = seasonData.reg │
│ ional_ranking === 1 ? "" : ` (${regionalPerc │
│ entile})`; │
│ + const percentileText = │
│ + seasonData.regional_ranking === 1 ? │
│ "" : ` (${regionalPercentile})`; │
│ rankingsText += `**Regional:** #${sea │
│ sonData.regional_ranking}${percentileText}\n │
│ `; │
│ } │
│ if (seasonData.realm_ranking) { │
│ - const percentileText = seasonData.rea │
│ lm_ranking === 1 ? "" : ` (${realmPercentile │
│ })`; │
│ + const percentileText = │
│ + seasonData.realm_ranking === 1 ? "" │
│ : ` (${realmPercentile})`; │
│ rankingsText += `**Realm:** #${season │
│ Data.realm_ranking}${percentileText}`; │
│ } │
│ │
│ @@ -113,10 +117,17 @@ export function create │
│ PlayerProfileEmbed(profile: PlayerProfileDat │
│ a): Embed { │
│ } │
│ } │
│ │
│ + // get class color for embed sidebar │
│ + const classKey = player.class_name │
│ + ?.toLowerCase() │
│ + .replace(/\s+/g, "_") as keyof typeof C │
│ LASS_COLORS; │
│ + const color = CLASS_COLORS[classKey] || 0 │
│ x5865f2; // fallback to discord blurple │
│ + │
│ return { │
│ title, │
│ description, │
│ fields, │
│ + color, │
│ footer: { │
│ text: "wowsimstats.com", │
│ }, │
└──────────────────────────────────────────────┘
┌─ web/src/scripts/deploy-commands.ts ───────────────────────────────────────┐
│ diff --git a/web/src/scripts/deploy-commands.ts b/web/src/scripts/deploy-commands. │
│ ts │
│ index acf1ff6..274fdfc 100644 │
│ --- a/web/src/scripts/deploy-commands.ts │
│ +++ b/web/src/scripts/deploy-commands.ts │
│ @@ -1,5 +1,5 @@ │
│ -// Discord command registration script │
│ -// Run this to register slash commands with Discord │
│ +// discord command registration script │
│ +// run this to register slash commands with Discord │
│ │
│ import { REST } from "@discordjs/rest"; │
│ import { Routes } from "discord-api-types/v10"; │
│ @@ -10,190 +10,191 @@ const DISCORD_APPLICATION_ID = process.env.DISCORD_APPLICATI │
│ ON_ID; │
│ const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN; │
│ │
│ if (!DISCORD_APPLICATION_ID || !DISCORD_BOT_TOKEN) { │
│ - console.error("Error: DISCORD_APPLICATION_ID and DISCORD_BOT_TOKEN must be set") │
│ ; │
│ - console.error("Please set these environment variables and try again."); │
│ - process.exit(1); │
│ + console.error( │
│ + "Error: DISCORD_APPLICATION_ID and DISCORD_BOT_TOKEN must be set", │
│ + ); │
│ + console.error("Please set these environment variables and try again."); │
│ + process.exit(1); │
│ } │
│ │
│ // define commands │
│ const commands = [ │
│ - // /player command │
│ - new SlashCommandBuilder() │
│ - .setName("player") │
│ - .setDescription("Look up a WoW Challenge Mode player") │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("name") │
│ - .setDescription("Player name") │
│ - .setRequired(true) │
│ - .setAutocomplete(true), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("region") │
│ - .setDescription("Region") │
│ - .setRequired(true) │
│ - .addChoices( │
│ - { name: "US", value: "us" }, │
│ - { name: "EU", value: "eu" }, │
│ - { name: "KR", value: "kr" }, │
│ - { name: "TW", value: "tw" }, │
│ - ), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("realm") │
│ - .setDescription("Realm") │
│ - .setRequired(true) │
│ - .setAutocomplete(true), │
│ - ), │
│ + // /player command │
│ + new SlashCommandBuilder() │
│ + .setName("player") │
│ + .setDescription("Look up a WoW Challenge Mode player") │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("name") │
│ + .setDescription("Player name") │
│ + .setRequired(true) │
│ + .setAutocomplete(true), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("region") │
│ + .setDescription("Region") │
│ + .setRequired(true) │
│ + .setAutocomplete(true), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("realm") │
│ + .setDescription("Realm") │
│ + .setRequired(true) │
│ + .setAutocomplete(true), │
│ + ), │
│ │
│ - // /leaderboard command │
│ - new SlashCommandBuilder() │
│ - .setName("leaderboard") │
│ - .setDescription("View WoW Challenge Mode leaderboards") │
│ - .addSubcommand((subcommand) => │
│ - subcommand │
│ - .setName("dungeon") │
│ - .setDescription("View dungeon leaderboard") │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("dungeon") │
│ - .setDescription("Choose a dungeon") │
│ - .setRequired(true) │
│ - .setAutocomplete(true), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("scope") │
│ - .setDescription("Leaderboard scope") │
│ - .setRequired(true) │
│ - .addChoices( │
│ - { name: "Global", value: "global" }, │
│ - { name: "Region", value: "region" }, │
│ - { name: "Realm", value: "realm" }, │
│ - ), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("region") │
│ - .setDescription("Region (required for regional/realm scope)") │
│ - .setRequired(false) │
│ - .addChoices( │
│ - { name: "US", value: "us" }, │
│ - { name: "EU", value: "eu" }, │
│ - { name: "KR", value: "kr" }, │
│ - { name: "TW", value: "tw" }, │
│ - ), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("realm") │
│ - .setDescription("Realm (required for realm scope)") │
│ - .setRequired(false) │
│ - .setAutocomplete(true), │
│ - ) │
│ - .addIntegerOption((option) => │
│ - option │
│ - .setName("limit") │
│ - .setDescription("Number of runs to display (default: 10, max: 15)") │
│ - .setRequired(false) │
│ - .setMinValue(1) │
│ - .setMaxValue(15), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("season") │
│ - .setDescription("Season (default: current season)") │
│ - .setRequired(false), │
│ - ), │
│ - ) │
│ - .addSubcommand((subcommand) => │
│ - subcommand │
│ - .setName("players") │
│ - .setDescription("View player rankings") │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("scope") │
│ - .setDescription("Leaderboard scope") │
│ - .setRequired(true) │
│ - .addChoices( │
│ - { name: "Global", value: "global" }, │
│ - { name: "Region", value: "region" }, │
│ - { name: "Realm", value: "realm" }, │
│ - ), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("region") │
│ - .setDescription("Region (required for regional/realm scope)") │
│ - .setRequired(false) │
│ - .addChoices( │
│ - { name: "US", value: "us" }, │
│ - { name: "EU", value: "eu" }, │
│ - { name: "KR", value: "kr" }, │
│ - { name: "TW", value: "tw" }, │
│ - ), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("realm") │
│ - .setDescription("Realm (required for realm scope)") │
│ - .setRequired(false) │
│ - .setAutocomplete(true), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("class") │
│ - .setDescription("Filter by class") │
│ - .setRequired(false) │
│ - .setAutocomplete(true), │
│ - ) │
│ - .addIntegerOption((option) => │
│ - option │
│ - .setName("limit") │
│ - .setDescription("Number of players to display (default: 25, max: 25)") │
│ - .setRequired(false) │
│ - .setMinValue(1) │
│ - .setMaxValue(25), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("season") │
│ - .setDescription("Season (default: current season)") │
│ - .setRequired(false), │
│ - ), │
│ - ), │
│ + // /leaderboard command │
│ + new SlashCommandBuilder() │
│ + .setName("leaderboard") │
│ + .setDescription("View WoW Challenge Mode leaderboards") │
│ + .addSubcommand((subcommand) => │
│ + subcommand │
│ + .setName("dungeon") │
│ + .setDescription("View dungeon leaderboard") │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("dungeon") │
│ + .setDescription("Choose a dungeon") │
│ + .setRequired(true) │
│ + .setAutocomplete(true), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("scope") │
│ + .setDescription("Leaderboard scope") │
│ + .setRequired(true) │
│ + .addChoices( │
│ + { name: "Global", value: "global" }, │
│ + { name: "Region", value: "region" }, │
│ + { name: "Realm", value: "realm" }, │
│ + ), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("region") │
│ + .setDescription("Region (required for regional/realm scope)") │
│ + .setRequired(false) │
│ + .addChoices( │
│ + { name: "US", value: "us" }, │
│ + { name: "EU", value: "eu" }, │
│ + { name: "KR", value: "kr" }, │
│ + { name: "TW", value: "tw" }, │
│ + ), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("realm") │
│ + .setDescription("Realm (required for realm scope)") │
│ + .setRequired(false) │
│ + .setAutocomplete(true), │
│ + ) │
│ + .addIntegerOption((option) => │
│ + option │
│ + .setName("limit") │
│ + .setDescription("Number of runs to display (default: 10, max: 15)") │
│ + .setRequired(false) │
│ + .setMinValue(1) │
│ + .setMaxValue(15), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("season") │
│ + .setDescription("Season (default: current season)") │
│ + .setRequired(false), │
│ + ), │
│ + ) │
│ + .addSubcommand((subcommand) => │
│ + subcommand │
│ + .setName("players") │
│ + .setDescription("View player rankings") │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("scope") │
│ + .setDescription("Leaderboard scope") │
│ + .setRequired(true) │
│ + .addChoices( │
│ + { name: "Global", value: "global" }, │
│ + { name: "Region", value: "region" }, │
│ + { name: "Realm", value: "realm" }, │
│ + ), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("region") │
│ + .setDescription("Region (required for regional/realm scope)") │
│ + .setRequired(false) │
│ + .addChoices( │
│ + { name: "US", value: "us" }, │
│ + { name: "EU", value: "eu" }, │
│ + { name: "KR", value: "kr" }, │
│ + { name: "TW", value: "tw" }, │
│ + ), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("realm") │
│ + .setDescription("Realm (required for realm scope)") │
│ + .setRequired(false) │
│ + .setAutocomplete(true), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("class") │
│ + .setDescription("Filter by class") │
│ + .setRequired(false) │
│ + .setAutocomplete(true), │
│ + ) │
│ + .addIntegerOption((option) => │
│ + option │
│ + .setName("limit") │
│ + .setDescription( │
│ + "Number of players to display (default: 25, max: 25)", │
│ + ) │
│ + .setRequired(false) │
│ + .setMinValue(1) │
│ + .setMaxValue(25), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("season") │
│ + .setDescription("Season (default: current season)") │
│ + .setRequired(false), │
│ + ), │
│ + ), │
│ ].map((command) => command.toJSON()); │
│ │
│ // register commands with Discord │
│ async function deployCommands() { │
│ - console.log("Registering Discord slash commands..."); │
│ - console.log(`Application ID: ${DISCORD_APPLICATION_ID}`); │
│ - console.log(`Commands to register: ${commands.length}`); │
│ + console.log("Registering Discord slash commands..."); │
│ + console.log(`Application ID: ${DISCORD_APPLICATION_ID}`); │
│ + console.log(`Commands to register: ${commands.length}`); │
│ │
│ - const rest = new REST({ version: "10" }).setToken(DISCORD_BOT_TOKEN); │
│ + const rest = new REST({ version: "10" }).setToken(DISCORD_BOT_TOKEN); │
│ │
│ - try { │
│ - console.log("Sending registration request to Discord..."); │
│ + try { │
│ + console.log("Sending registration request to Discord..."); │
│ │
│ - const data = await rest.put( │
│ - Routes.applicationCommands(DISCORD_APPLICATION_ID), │
│ - { body: commands }, │
│ - ) as any[]; │
│ + const data = (await rest.put( │
│ + Routes.applicationCommands(DISCORD_APPLICATION_ID), │
│ + { body: commands }, │
│ + )) as any[]; │
│ │
│ - console.log(`? Successfully registered ${data.length} slash commands!`); │
│ - console.log("\nRegistered commands:"); │
│ - data.forEach((cmd: any) => { │
│ - console.log(` - /${cmd.name}: ${cmd.description}`); │
│ - }); │
│ + console.log(`? Successfully registered ${data.length} slash commands!`); │
│ + console.log("\nRegistered commands:"); │
│ + data.forEach((cmd: any) => { │
│ + console.log(` - /${cmd.name}: ${cmd.description}`); │
│ + }); │
│ │
│ - console.log("\nYou can now use these commands in Discord!"); │
│ - console.log("Note: It may take a few minutes for commands to appear in all serv │
│ ers."); │
│ - } catch (error) { │
│ - console.error("? Error registering commands:", error); │
│ - process.exit(1); │
│ - } │
│ + console.log("\nYou can now use these commands in Discord!"); │
│ + console.log( │
│ + "Note: It may take a few minutes for commands to appear in all servers.", │
│ + ); │
│ + } catch (error) { │
│ + console.error("? Error registering commands:", error); │
│ + process.exit(1); │
│ + } │
│ } │
│ │
│ // run the deployment │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...rc/scripts/deploy-commands.ts ───┐
│ diff --git a/web/src/scripts/deploy-commands │
│ .ts b/web/src/scripts/deploy-commands.ts │
│ index acf1ff6..274fdfc 100644 │
│ --- a/web/src/scripts/deploy-commands.ts │
│ +++ b/web/src/scripts/deploy-commands.ts │
│ @@ -1,5 +1,5 @@ │
│ -// Discord command registration script │
│ -// Run this to register slash commands with │
│ Discord │
│ +// discord command registration script │
│ +// run this to register slash commands with │
│ Discord │
│ │
│ import { REST } from "@discordjs/rest"; │
│ import { Routes } from "discord-api-types/v │
│ 10"; │
│ @@ -10,190 +10,191 @@ const DISCORD_APPLICAT │
│ ION_ID = process.env.DISCORD_APPLICATION_ID; │
│ const DISCORD_BOT_TOKEN = process.env.DISCO │
│ RD_BOT_TOKEN; │
│ │
│ if (!DISCORD_APPLICATION_ID || !DISCORD_BOT │
│ _TOKEN) { │
│ - console.error("Error: DISCORD_APPLICATION_ │
│ ID and DISCORD_BOT_TOKEN must be set"); │
│ - console.error("Please set these environmen │
│ t variables and try again."); │
│ - process.exit(1); │
│ + console.error( │
│ + "Error: DISCORD_APPLICATION_ID and DISC │
│ ORD_BOT_TOKEN must be set", │
│ + ); │
│ + console.error("Please set these environme │
│ nt variables and try again."); │
│ + process.exit(1); │
│ } │
│ │
│ // define commands │
│ const commands = [ │
│ - // /player command │
│ - new SlashCommandBuilder() │
│ - .setName("player") │
│ - .setDescription("Look up a WoW Challenge │
│ Mode player") │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("name") │
│ - .setDescription("Player name") │
│ - .setRequired(true) │
│ - .setAutocomplete(true), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("region") │
│ - .setDescription("Region") │
│ - .setRequired(true) │
│ - .addChoices( │
│ - { name: "US", value: "us" }, │
│ - { name: "EU", value: "eu" }, │
│ - { name: "KR", value: "kr" }, │
│ - { name: "TW", value: "tw" }, │
│ - ), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("realm") │
│ - .setDescription("Realm") │
│ - .setRequired(true) │
│ - .setAutocomplete(true), │
│ - ), │
│ + // /player command │
│ + new SlashCommandBuilder() │
│ + .setName("player") │
│ + .setDescription("Look up a WoW Challeng │
│ e Mode player") │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("name") │
│ + .setDescription("Player name") │
│ + .setRequired(true) │
│ + .setAutocomplete(true), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("region") │
│ + .setDescription("Region") │
│ + .setRequired(true) │
│ + .setAutocomplete(true), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("realm") │
│ + .setDescription("Realm") │
│ + .setRequired(true) │
│ + .setAutocomplete(true), │
│ + ), │
│ │
│ - // /leaderboard command │
│ - new SlashCommandBuilder() │
│ - .setName("leaderboard") │
│ - .setDescription("View WoW Challenge Mode │
│ leaderboards") │
│ - .addSubcommand((subcommand) => │
│ - subcommand │
│ - .setName("dungeon") │
│ - .setDescription("View dungeon leaderboa │
│ rd") │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("dungeon") │
│ - .setDescription("Choose a dungeon") │
│ - .setRequired(true) │
│ - .setAutocomplete(true), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("scope") │
│ - .setDescription("Leaderboard scope") │
│ - .setRequired(true) │
│ - .addChoices( │
│ - { name: "Global", value: "global" }, │
│ - { name: "Region", value: "region" }, │
│ - { name: "Realm", value: "realm" }, │
│ - ), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("region") │
│ - .setDescription("Region (required for │
│ regional/realm scope)") │
│ - .setRequired(false) │
│ - .addChoices( │
│ - { name: "US", value: "us" }, │
│ - { name: "EU", value: "eu" }, │
│ - { name: "KR", value: "kr" }, │
│ - { name: "TW", value: "tw" }, │
│ - ), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("realm") │
│ - .setDescription("Realm (required for │
│ realm scope)") │
│ - .setRequired(false) │
│ - .setAutocomplete(true), │
│ - ) │
│ - .addIntegerOption((option) => │
│ - option │
│ - .setName("limit") │
│ - .setDescription("Number of runs to di │
│ splay (default: 10, max: 15)") │
│ - .setRequired(false) │
│ - .setMinValue(1) │
│ - .setMaxValue(15), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("season") │
│ - .setDescription("Season (default: cur │
│ rent season)") │
│ - .setRequired(false), │
│ - ), │
│ - ) │
│ - .addSubcommand((subcommand) => │
│ - subcommand │
│ - .setName("players") │
│ - .setDescription("View player rankings") │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("scope") │
│ - .setDescription("Leaderboard scope") │
│ - .setRequired(true) │
│ - .addChoices( │
│ - { name: "Global", value: "global" }, │
│ - { name: "Region", value: "region" }, │
│ - { name: "Realm", value: "realm" }, │
│ - ), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("region") │
│ - .setDescription("Region (required for │
│ regional/realm scope)") │
│ - .setRequired(false) │
│ - .addChoices( │
│ - { name: "US", value: "us" }, │
│ - { name: "EU", value: "eu" }, │
│ - { name: "KR", value: "kr" }, │
│ - { name: "TW", value: "tw" }, │
│ - ), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("realm") │
│ - .setDescription("Realm (required for │
│ realm scope)") │
│ - .setRequired(false) │
│ - .setAutocomplete(true), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("class") │
│ - .setDescription("Filter by class") │
│ - .setRequired(false) │
│ - .setAutocomplete(true), │
│ - ) │
│ - .addIntegerOption((option) => │
│ - option │
│ - .setName("limit") │
│ - .setDescription("Number of players to │
│ display (default: 25, max: 25)") │
│ - .setRequired(false) │
│ - .setMinValue(1) │
│ - .setMaxValue(25), │
│ - ) │
│ - .addStringOption((option) => │
│ - option │
│ - .setName("season") │
│ - .setDescription("Season (default: cur │
│ rent season)") │
│ - .setRequired(false), │
│ - ), │
│ - ), │
│ + // /leaderboard command │
│ + new SlashCommandBuilder() │
│ + .setName("leaderboard") │
│ + .setDescription("View WoW Challenge Mod │
│ e leaderboards") │
│ + .addSubcommand((subcommand) => │
│ + subcommand │
│ + .setName("dungeon") │
│ + .setDescription("View dungeon leade │
│ rboard") │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("dungeon") │
│ + .setDescription("Choose a dunge │
│ on") │
│ + .setRequired(true) │
│ + .setAutocomplete(true), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("scope") │
│ + .setDescription("Leaderboard sc │
│ ope") │
│ + .setRequired(true) │
│ + .addChoices( │
│ + { name: "Global", value: "glo │
│ bal" }, │
│ + { name: "Region", value: "reg │
│ ion" }, │
│ + { name: "Realm", value: "real │
│ m" }, │
│ + ), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("region") │
│ + .setDescription("Region (requir │
│ ed for regional/realm scope)") │
│ + .setRequired(false) │
│ + .addChoices( │
│ + { name: "US", value: "us" }, │
│ + { name: "EU", value: "eu" }, │
│ + { name: "KR", value: "kr" }, │
│ + { name: "TW", value: "tw" }, │
│ + ), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("realm") │
│ + .setDescription("Realm (require │
│ d for realm scope)") │
│ + .setRequired(false) │
│ + .setAutocomplete(true), │
│ + ) │
│ + .addIntegerOption((option) => │
│ + option │
│ + .setName("limit") │
│ + .setDescription("Number of runs │
│ to display (default: 10, max: 15)") │
│ + .setRequired(false) │
│ + .setMinValue(1) │
│ + .setMaxValue(15), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("season") │
│ + .setDescription("Season (defaul │
│ t: current season)") │
│ + .setRequired(false), │
│ + ), │
│ + ) │
│ + .addSubcommand((subcommand) => │
│ + subcommand │
│ + .setName("players") │
│ + .setDescription("View player rankin │
│ gs") │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("scope") │
│ + .setDescription("Leaderboard sc │
│ ope") │
│ + .setRequired(true) │
│ + .addChoices( │
│ + { name: "Global", value: "glo │
│ bal" }, │
│ + { name: "Region", value: "reg │
│ ion" }, │
│ + { name: "Realm", value: "real │
│ m" }, │
│ + ), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("region") │
│ + .setDescription("Region (requir │
│ ed for regional/realm scope)") │
│ + .setRequired(false) │
│ + .addChoices( │
│ + { name: "US", value: "us" }, │
│ + { name: "EU", value: "eu" }, │
│ + { name: "KR", value: "kr" }, │
│ + { name: "TW", value: "tw" }, │
│ + ), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("realm") │
│ + .setDescription("Realm (require │
│ d for realm scope)") │
│ + .setRequired(false) │
│ + .setAutocomplete(true), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("class") │
│ + .setDescription("Filter by clas │
│ s") │
│ + .setRequired(false) │
│ + .setAutocomplete(true), │
│ + ) │
│ + .addIntegerOption((option) => │
│ + option │
│ + .setName("limit") │
│ + .setDescription( │
│ + "Number of players to display │
│ (default: 25, max: 25)", │
│ + ) │
│ + .setRequired(false) │
│ + .setMinValue(1) │
│ + .setMaxValue(25), │
│ + ) │
│ + .addStringOption((option) => │
│ + option │
│ + .setName("season") │
│ + .setDescription("Season (defaul │
│ t: current season)") │
│ + .setRequired(false), │
│ + ), │
│ + ), │
│ ].map((command) => command.toJSON()); │
│ │
│ // register commands with Discord │
│ async function deployCommands() { │
│ - console.log("Registering Discord slash com │
│ mands..."); │
│ - console.log(`Application ID: ${DISCORD_APP │
│ LICATION_ID}`); │
│ - console.log(`Commands to register: ${comma │
│ nds.length}`); │
│ + console.log("Registering Discord slash co │
│ mmands..."); │
│ + console.log(`Application ID: ${DISCORD_AP │
│ PLICATION_ID}`); │
│ + console.log(`Commands to register: ${comm │
│ ands.length}`); │
│ │
│ - const rest = new REST({ version: "10" }).s │
│ etToken(DISCORD_BOT_TOKEN); │
│ + const rest = new REST({ version: "10" }). │
│ setToken(DISCORD_BOT_TOKEN); │
│ │
│ - try { │
│ - console.log("Sending registration request │
│ to Discord..."); │
│ + try { │
│ + console.log("Sending registration reque │
│ st to Discord..."); │
│ │
│ - const data = await rest.put( │
│ - Routes.applicationCommands(DISCORD_APPLI │
│ CATION_ID), │
│ - { body: commands }, │
│ - ) as any[]; │
│ + const data = (await rest.put( │
│ + Routes.applicationCommands(DISCORD_AP │
│ PLICATION_ID), │
│ + { body: commands }, │
│ + )) as any[]; │
│ │
│ - console.log(`? Successfully registered ${ │
│ data.length} slash commands!`); │
│ - console.log("\nRegistered commands:"); │
│ - data.forEach((cmd: any) => { │
│ - console.log(` - /${cmd.name}: ${cmd.des │
│ cription}`); │
│ - }); │
│ + console.log(`? Successfully registered │
│ ${data.length} slash commands!`); │
│ + console.log("\nRegistered commands:"); │
│ + data.forEach((cmd: any) => { │
│ + console.log(` - /${cmd.name}: ${cmd. │
│ description}`); │
│ + }); │
│ │
│ - console.log("\nYou can now use these comm │
│ ands in Discord!"); │
│ - console.log("Note: It may take a few minu │
│ tes for commands to appear in all servers.") │
│ ; │
│ - } catch (error) { │
│ - console.error("? Error registering comman │
│ ds:", error); │
│ - process.exit(1); │
│ - } │
│ + console.log("\nYou can now use these co │
│ mmands in Discord!"); │
│ + console.log( │
│ + "Note: It may take a few minutes for │
│ commands to appear in all servers.", │
│ + ); │
│ + } catch (error) { │
│ + console.error("? Error registering comm │
│ ands:", error); │
│ + process.exit(1); │
│ + } │
│ } │
│ │
│ // run the deployment │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET