Last active 2 weeks ago

Revision 17c0baf25a440230ca074f654e41f7ff0aa2e357

autocomplete.ts Raw
1import { AutocompleteInteraction } from "discord.js";
2import { resolveUser } from "@systems/users";
3import { getCharacters } from "@systems/characters";
4import { Config } from "@systems/config";
5import { Emoji } from "@systems/emojis";
6import { CharacterRegistry } from "@registry/character-registry";
7import { UserRegistry } from "@registry/user-registry";
8import { Paths } from "@helpers/paths";
9import { Nation } from "@types";
10import { NATION_UNICODE } from "@systems/nations";
11import { autocompleteLayout } from "@subcommands/tg-config/set-layout";
12import { UpdatesCommands } from "@subcommands/admin/updates";
13import { ResultCommands } from "@subcommands/admin/result-post";
14import { SetResultLayoutCommands } from "@subcommands/tg-config/set-result-layout";
15import { SetLeaderboardLayoutCommands } from "@subcommands/tg-config/set-leaderboard-layout";
16import fs from "fs";
17
18// ─── Usermap cache ────────────────────────────────────────────────────────────
19let _usermapCache: Record<string, any> | null = null;
20
21function getUsermapCache(): Record<string, any> {
22 if (!_usermapCache) {
23 try { _usermapCache = JSON.parse(fs.readFileSync(Paths.data("usermap.json"), "utf8")); }
24 catch { _usermapCache = {}; }
25 }
26 return _usermapCache!;
27}
28
29export function invalidateUsermapCache(): void { _usermapCache = null; }
30
31// ─── Autocomplete subsets ─────────────────────────────────────────────────────
32
33async function autocompleteCharNames(
34 interaction: AutocompleteInteraction,
35 focused: string,
36 nation?: Nation | null // optional — if provided, filter by nation
37): Promise<void> {
38 const member = await interaction.guild!.members.fetch(interaction.user.id);
39 const user = await resolveUser(member);
40
41 // For bringer set — scan all chars by nation, no user filter needed
42 if (nation !== undefined) {
43 const all = CharacterRegistry.all();
44 const results = all
45 .filter((c) => !nation || c.nation === nation)
46 .filter((c) => c.name.toLowerCase().includes(focused.toLowerCase()))
47 .map((c) => {
48 const nationEmoji = c.nation ? (NATION_UNICODE[c.nation] || c.nation) : "";
49 return { name: `${c.class} ${c.level} ${c.name} ${nationEmoji}`.trim(), value: c.name };
50 })
51 .slice(0, 25);
52 return interaction.respond(results);
53 }
54
55 if (!user.userKey) return interaction.respond([]);
56
57 // Own chars
58 const ownChars = getCharacters(user.userKey).map((c) => {
59 const nationEmoji = c.nation ? (Emoji.nation(c.nation) || c.nation) : "";
60 return {
61 name: `${c.class} ${c.level} ${c.name} ${nationEmoji}`.trim(),
62 value: c.name,
63 };
64 });
65
66 // Shared chars
67 const sharedChars = CharacterRegistry.sharedWith(user.userKey).map(({ char }) => {
68 const nationEmoji = char.nation ? (Emoji.nation(char.nation) || char.nation) : "";
69 return {
70 name: `${char.class} ${char.level} ${char.name} 🔗 ${nationEmoji}`.trim(),
71 value: char.name,
72 };
73 });
74
75 const all = [...ownChars, ...sharedChars]
76 .filter((c) => c.name.toLowerCase().includes(focused.toLowerCase()))
77 .slice(0, 25);
78
79 await interaction.respond(all);
80}
81
82async function autocompleteUserKeys(
83 interaction: AutocompleteInteraction,
84 focused: string
85): Promise<void> {
86 try {
87 const usermap = getUsermapCache();
88 const choices = Object.entries(usermap)
89 .map(([, entry]: [string, any]) => {
90 const fileKey = typeof entry === "string" ? entry : entry.file;
91 const alias = typeof entry === "object" ? (entry.aliases?.[0] ?? fileKey) : fileKey;
92 return { name: `${alias} (${fileKey})`, value: fileKey };
93 })
94 .filter((c) => c.name.toLowerCase().includes(focused.toLowerCase()))
95 .slice(0, 25);
96 await interaction.respond(choices);
97 } catch {
98 await interaction.respond([]);
99 }
100}
101
102async function autocompleteSlots(
103 interaction: AutocompleteInteraction,
104 focused: string
105): Promise<void> {
106 const slots = Config.get({ section: "poll", key: "slots" })
107 .filter((s) => s.active)
108 .map((s) => ({ name: `${s.tgHour}:00`, value: String(s.tgHour) }))
109 .filter((s) => s.name.includes(focused));
110 await interaction.respond(slots);
111}
112
113// ─── Router ───────────────────────────────────────────────────────────────────
114
115export async function handleAutocomplete(interaction: AutocompleteInteraction): Promise<void> {
116 try {
117 const focused = interaction.options.getFocused(true);
118 const optionName = focused.name;
119 const focusedValue = focused.value as string;
120 const sub = interaction.options.getSubcommand(false);
121 const subGroup = interaction.options.getSubcommandGroup(false);
122
123 if (optionName === "char_name") {
124 // Bringer set — filter by selected nation
125 if (sub === "set" && subGroup === "bringer") {
126 const nation = interaction.options.getString("nation") as Nation | null;
127 return await autocompleteCharNames(interaction, focusedValue, nation);
128 }
129 return await autocompleteCharNames(interaction, focusedValue);
130 }
131
132 if (optionName === "name") return await autocompleteUserKeys(interaction, focusedValue);
133 if (optionName === "slot") return await autocompleteSlots(interaction, focusedValue);
134 if (optionName === "owner") return await autocompleteUserKeys(interaction, focusedValue);
135 if (optionName === "layout") return await autocompleteLayout(interaction);
136 if (optionName === "version") return UpdatesCommands.autocomplete(interaction);
137 if (optionName === "history_key") return await ResultCommands.autocompleteHistory(interaction);
138 if (optionName === "week_key") return await ResultCommands.autocompleteWeekKey(interaction);
139
140 if (optionName === "layout") {
141 if (sub === "set-result-layout") return await SetResultLayoutCommands.autocomplete(interaction);
142 if (sub === "set-leaderboard-layout") return await SetLeaderboardLayoutCommands.autocomplete(interaction);
143 return await autocompleteLayout(interaction); // poll default
144 }
145
146 await interaction.respond([]);
147 } catch (err) {
148 console.error("[autocomplete] error:", err);
149 try { await interaction.respond([]); } catch {}
150 }
151}