Ultima attività 1 month ago

Revisione 5cfa4d69ec0534e5a1f6f8a46e6c52154e71e66a

tg.ts Raw
1import {
2 ChatInputCommandInteraction,
3 SlashCommandBuilder,
4 REST,
5 Routes,
6} from "discord.js";
7import { cfg } from "../systems/config";
8import { hasOfficerRole } from "../systems/users";
9
10// Poll subcommands
11import { handleStart } from "../subcommands/poll/start";
12import { handleLock } from "../subcommands/poll/lock";
13import { handleUnlock } from "../subcommands/poll/unlock";
14import { handleConfirm } from "../subcommands/poll/confirm";
15import { handleStatus } from "../subcommands/poll/status";
16import { handleReload } from "../subcommands/poll/reload";
17import { handleSetMessage, handleClearMessage, handleSetEphemeral, handleClearEphemeral } from "../subcommands/poll/setMessage";
18import { handleInject, handleRemoveVote } from "../subcommands/poll/inject";
19import { handleSeed } from "../subcommands/poll/seed";
20import { handlePurge } from "../subcommands/poll/purge";
21import { handleImpersonate } from "../subcommands/impersonate";
22
23// Char subcommands (borrow / sharing system)
24import { handleCharBorrow } from "../subcommands/char/borrow";
25import { handleCharAccept } from "../subcommands/char/accept";
26import { handleCharDecline } from "../subcommands/char/decline";
27import { handleCharShare, handleCharUnshare } from "../subcommands/char/share";
28
29// Score subcommands
30import { handleScoreSet } from "../subcommands/score/set";
31import { handleScoreGet } from "../subcommands/score/get";
32
33// Rank subcommands
34import { handleRankGet } from "../subcommands/rank/get";
35import { handleRankPost } from "../subcommands/rank/post";
36
37// Result subcommands
38import { handleResultSet } from "../subcommands/result/set";
39import { handleResultView } from "../subcommands/result/view";
40import { handleResultPost } from "../subcommands/result/post";
41
42// Bringer subcommands
43import { handleBringerSet } from "../subcommands/bringer/set";
44import { handleBringerClear } from "../subcommands/bringer/clear";
45
46// Other
47import { handleSwitch } from "../subcommands/switch";
48import { handleHistory } from "../subcommands/history";
49
50export function buildTgCommand(): SlashCommandBuilder {
51 const cmd = new SlashCommandBuilder()
52 .setName("tg")
53 .setDescription("TG planning and tracking");
54
55 // ── poll group ─────────────────────────────────────────────────────────────
56 cmd.addSubcommandGroup((g) => g
57 .setName("poll")
58 .setDescription("Manage the TG poll")
59 .addSubcommand((s) => s.setName("start").setDescription("Post a fresh TG poll")
60 .addStringOption((o) => o.setName("slot").setDescription("TG hour (e.g. 20, 22)").setRequired(false)))
61 .addSubcommand((s) => s.setName("lock").setDescription("Lock the active poll")
62 .addStringOption((o) => o.setName("message").setDescription("One-time lock message").setRequired(false)))
63 .addSubcommand((s) => s.setName("unlock").setDescription("Unlock the active poll"))
64 .addSubcommand((s) => s.setName("confirm").setDescription("Confirm whether TG is happening")
65 .addStringOption((o) => o.setName("decision").setDescription("yes or no").setRequired(true)
66 .addChoices({ name: "Yes", value: "yes" }, { name: "No", value: "no" }))
67 .addStringOption((o) => o.setName("message").setDescription("One-time confirm message").setRequired(false))
68 .addBooleanOption((o) => o.setName("tag").setDescription("Tag configured roles?").setRequired(false)))
69 .addSubcommand((s) => s.setName("reload").setDescription("Reload messages and emojis from disk"))
70 .addSubcommand((s) => s.setName("status").setDescription("Show current poll and config status"))
71 .addSubcommand((s) => s.setName("set-message").setDescription("Set public message override for a user")
72 .addStringOption((o) => o.setName("vote_type").setDescription("yes or no").setRequired(true)
73 .addChoices({ name: "Yes", value: "yes" }, { name: "No", value: "no" }))
74 .addStringOption((o) => o.setName("message").setDescription("Message to show").setRequired(true))
75 .addStringOption((o) => o.setName("name").setDescription("Usermap key (officer only)").setRequired(false)))
76 .addSubcommand((s) => s.setName("clear-message").setDescription("Clear public message override")
77 .addStringOption((o) => o.setName("vote_type").setDescription("yes or no").setRequired(false)
78 .addChoices({ name: "Yes", value: "yes" }, { name: "No", value: "no" }))
79 .addStringOption((o) => o.setName("name").setDescription("Usermap key (officer only)").setRequired(false)))
80 .addSubcommand((s) => s.setName("set-ephemeral").setDescription("Set ephemeral message override for a user")
81 .addStringOption((o) => o.setName("vote_type").setDescription("yes or no").setRequired(true)
82 .addChoices({ name: "Yes", value: "yes" }, { name: "No", value: "no" }))
83 .addStringOption((o) => o.setName("message").setDescription("Message to show").setRequired(true))
84 .addStringOption((o) => o.setName("name").setDescription("Usermap key (officer only)").setRequired(false)))
85 .addSubcommand((s) => s.setName("clear-ephemeral").setDescription("Clear ephemeral message override")
86 .addStringOption((o) => o.setName("vote_type").setDescription("yes or no").setRequired(false)
87 .addChoices({ name: "Yes", value: "yes" }, { name: "No", value: "no" }))
88 .addStringOption((o) => o.setName("name").setDescription("Usermap key (officer only)").setRequired(false)))
89 .addSubcommand((s) => s.setName("inject").setDescription("Inject a vote for a registered user")
90 .addStringOption((o) => o.setName("name").setDescription("Usermap key").setRequired(true))
91 .addStringOption((o) => o.setName("vote_type").setDescription("yes or no").setRequired(true)
92 .addChoices({ name: "Yes", value: "yes" }, { name: "No", value: "no" })))
93 .addSubcommand((s) => s.setName("remove-vote").setDescription("Remove a vote for a registered user")
94 .addStringOption((o) => o.setName("name").setDescription("Usermap key").setRequired(true)))
95 .addSubcommand((s) => s.setName("purge").setDescription("Delete all bot messages from the poll channel"))
96 .addSubcommand((s) => s.setName("seed").setDescription("Inject all registered players as Yes votes for layout testing"))
97 );
98
99 // ── score group ────────────────────────────────────────────────────────────
100 cmd.addSubcommandGroup((g) => g
101 .setName("score")
102 .setDescription("Score management")
103 .addSubcommand((s) => s.setName("set").setDescription("Submit a score")
104 .addIntegerOption((o) => o.setName("pts").setDescription("Points").setRequired(true))
105 .addStringOption((o) => o.setName("slot").setDescription("TG hour (e.g. 20, 8pm, midnight)").setRequired(false))
106 .addIntegerOption((o) => o.setName("k").setDescription("Kills").setRequired(false))
107 .addIntegerOption((o) => o.setName("d").setDescription("Deaths").setRequired(false))
108 .addIntegerOption((o) => o.setName("atk").setDescription("Attack score").setRequired(false))
109 .addIntegerOption((o) => o.setName("def").setDescription("Defense score").setRequired(false))
110 .addIntegerOption((o) => o.setName("heal").setDescription("Healing score (FA only)").setRequired(false))
111 .addStringOption((o) => o.setName("name").setDescription("Usermap key (officer only)").setRequired(false)))
112 .addSubcommand((s) => s.setName("get").setDescription("View a score")
113 .addStringOption((o) => o.setName("slot").setDescription("TG hour").setRequired(false))
114 .addStringOption((o) => o.setName("name").setDescription("Usermap key (officer only)").setRequired(false)))
115 );
116
117 // ── rank group ─────────────────────────────────────────────────────────────
118 cmd.addSubcommandGroup((g) => g
119 .setName("rank")
120 .setDescription("W.Rank management")
121 .addSubcommand((s) => s.setName("get").setDescription("View W.Rank")
122 .addStringOption((o) => o.setName("name").setDescription("Usermap key (officer only)").setRequired(false)))
123 .addSubcommand((s) => s.setName("post").setDescription("Post leaderboard publicly (officer only)"))
124 );
125
126 // ── result group ───────────────────────────────────────────────────────────
127 cmd.addSubcommandGroup((g) => g
128 .setName("result")
129 .setDescription("TG result management")
130 .addSubcommand((s) => s.setName("set").setDescription("Set nation K/D (officer only)")
131 .addStringOption((o) => o.setName("nation").setDescription("Source nation").setRequired(true)
132 .addChoices({ name: "Capella", value: "Capella" }, { name: "Procyon", value: "Procyon" }))
133 .addIntegerOption((o) => o.setName("kills").setDescription("Kills").setRequired(true))
134 .addIntegerOption((o) => o.setName("deaths").setDescription("Deaths").setRequired(true))
135 .addStringOption((o) => o.setName("slot").setDescription("TG hour").setRequired(false)))
136 .addSubcommand((s) => s.setName("view").setDescription("View result for a slot")
137 .addStringOption((o) => o.setName("slot").setDescription("TG hour").setRequired(false)))
138 .addSubcommand((s) => s.setName("post").setDescription("Post result publicly (officer only)")
139 .addStringOption((o) => o.setName("slot").setDescription("TG hour").setRequired(false)))
140 );
141
142 // ── bringer group ──────────────────────────────────────────────────────────
143 cmd.addSubcommandGroup((g) => g
144 .setName("bringer")
145 .setDescription("Bringer management (officer only)")
146 .addSubcommand((s) => s.setName("set").setDescription("Manually set Bringer")
147 .addStringOption((o) => o.setName("nation").setDescription("Nation").setRequired(true)
148 .addChoices({ name: "Capella", value: "Capella" }, { name: "Procyon", value: "Procyon" }))
149 .addStringOption((o) => o.setName("name").setDescription("Usermap key").setRequired(true)))
150 .addSubcommand((s) => s.setName("clear").setDescription("Clear Bringer override")
151 .addStringOption((o) => o.setName("nation").setDescription("Nation").setRequired(true)
152 .addChoices({ name: "Capella", value: "Capella" }, { name: "Procyon", value: "Procyon" })))
153 );
154
155 // ── switch ─────────────────────────────────────────────────────────────────
156 cmd.addSubcommand((s) => s.setName("switch").setDescription("Switch active character")
157 .addStringOption((o) => o.setName("char_name").setDescription("Character name").setRequired(true))
158 .addStringOption((o) => o.setName("name").setDescription("Usermap key (officer only)").setRequired(false))
159 );
160
161 // ── char group ─────────────────────────────────────────────────────────────
162 cmd.addSubcommandGroup((g) => g
163 .setName("char")
164 .setDescription("Character management")
165 .addSubcommand((s) => s.setName("add").setDescription("Add a character")
166 .addStringOption((o) => o.setName("char_name").setDescription("Character name").setRequired(true))
167 .addStringOption((o) => o.setName("class").setDescription("Class").setRequired(true)
168 .addChoices(
169 { name: "Blader (BL)", value: "BL" },
170 { name: "Force Blader (FB)", value: "FB" },
171 { name: "Force Shielder (FS)", value: "FS" },
172 { name: "Force Archer (FA)", value: "FA" },
173 { name: "Force Gunner (FG)", value: "FG" },
174 { name: "Gladiator (GL)", value: "GL" },
175 { name: "Dark Mage (DM)", value: "DM" },
176 { name: "Wizard (WI)", value: "WI" },
177 { name: "Warrior (WA)", value: "WA" },
178 ))
179 .addIntegerOption((o) => o.setName("level").setDescription("Level").setRequired(true))
180 .addStringOption((o) => o.setName("nation").setDescription("Nation").setRequired(true)
181 .addChoices({ name: "Capella", value: "Capella" }, { name: "Procyon", value: "Procyon" }))
182 .addStringOption((o) => o.setName("name").setDescription("Usermap key (officer only)").setRequired(false)))
183 .addSubcommand((s) => s.setName("remove").setDescription("Remove a character")
184 .addStringOption((o) => o.setName("char_name").setDescription("Character name").setRequired(true))
185 .addStringOption((o) => o.setName("name").setDescription("Usermap key (officer only)").setRequired(false)))
186 .addSubcommand((s) => s.setName("set-active").setDescription("Set active character")
187 .addStringOption((o) => o.setName("char_name").setDescription("Character name").setRequired(true))
188 .addStringOption((o) => o.setName("name").setDescription("Usermap key (officer only)").setRequired(false)))
189 .addSubcommand((s) => s.setName("set-nation").setDescription("Change a character's nation")
190 .addStringOption((o) => o.setName("nation").setDescription("Nation").setRequired(true)
191 .addChoices({ name: "Capella", value: "Capella" }, { name: "Procyon", value: "Procyon" }))
192 .addStringOption((o) => o.setName("char_name").setDescription("Character name (defaults to active)").setRequired(false))
193 .addStringOption((o) => o.setName("name").setDescription("Usermap key (officer only)").setRequired(false)))
194 .addSubcommand((s) => s.setName("set-stats").setDescription("Set character combat stats")
195 .addStringOption((o) => o.setName("char_name").setDescription("Character name (defaults to active)").setRequired(false))
196 .addIntegerOption((o) => o.setName("atk").setDescription("Attack score").setRequired(false))
197 .addIntegerOption((o) => o.setName("def").setDescription("Defense score").setRequired(false))
198 .addIntegerOption((o) => o.setName("heal").setDescription("Healing score").setRequired(false))
199 .addStringOption((o) => o.setName("name").setDescription("Usermap key (officer only)").setRequired(false)))
200 .addSubcommand((s) => s.setName("borrow").setDescription("Request to borrow a character for this session")
201 .addStringOption((o) => o.setName("owner").setDescription("Owner's usermap key").setRequired(true))
202 .addStringOption((o) => o.setName("char_name").setDescription("Character name").setRequired(true))
203 .addStringOption((o) => o.setName("name").setDescription("Grant to this user (officer only)").setRequired(false)))
204 .addSubcommand((s) => s.setName("accept").setDescription("Accept a borrow request")
205 .addStringOption((o) => o.setName("name").setDescription("Requester's usermap key").setRequired(true)))
206 .addSubcommand((s) => s.setName("decline").setDescription("Decline a borrow request")
207 .addStringOption((o) => o.setName("name").setDescription("Requester's usermap key").setRequired(true)))
208 .addSubcommand((s) => s.setName("share").setDescription("Permanently share a character")
209 .addStringOption((o) => o.setName("char_name").setDescription("Character name").setRequired(true))
210 .addStringOption((o) => o.setName("name").setDescription("Usermap key to share with").setRequired(true))
211 .addStringOption((o) => o.setName("owner").setDescription("Owner's usermap key (officer only)").setRequired(false)))
212 .addSubcommand((s) => s.setName("unshare").setDescription("Revoke permanent character share")
213 .addStringOption((o) => o.setName("char_name").setDescription("Character name").setRequired(true))
214 .addStringOption((o) => o.setName("name").setDescription("Usermap key to revoke").setRequired(true))
215 .addStringOption((o) => o.setName("owner").setDescription("Owner's usermap key (officer only)").setRequired(false)))
216 .addSubcommand((s) => s.setName("active").setDescription("Check active character for a user")
217 .addStringOption((o) => o.setName("name").setDescription("Usermap key (officer: check others)").setRequired(false)))
218);
219
220 // ── history ────────────────────────────────────────────────────────────────
221 cmd.addSubcommand((s) => s.setName("history").setDescription("View TG history (officer only)")
222 .addStringOption((o) => o.setName("date").setDescription("Date (YYYY-MM-DD)").setRequired(false))
223 .addStringOption((o) => o.setName("slot").setDescription("TG hour").setRequired(false))
224 );
225
226 // ── impersonate ────────────────────────────────────────────────────────────────
227 cmd.addSubcommand((s) => s.setName("impersonate").setDescription("Impersonate a registered user for testing (officer only)"));
228
229 return cmd;
230}
231
232export async function handleTgCommand(interaction: ChatInputCommandInteraction): Promise<void> {
233 const group = interaction.options.getSubcommandGroup(false);
234 const sub = interaction.options.getSubcommand();
235 const member = await interaction.guild!.members.fetch(interaction.user.id);
236 const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
237
238 // Officer-only commands
239 const officerOnlyGroups = ["poll", "result", "bringer"];
240 const officerOnlySubs = ["history"];
241 const officerOnlyRankSubs = ["post"];
242
243 if (group && officerOnlyGroups.includes(group) && !isOfficer) {
244 return void interaction.reply({ content: "❌ You don't have permission to use this command.", ephemeral: true });
245 }
246 if (!group && officerOnlySubs.includes(sub) && !isOfficer) {
247 return void interaction.reply({ content: "❌ You don't have permission to use this command.", ephemeral: true });
248 }
249 if (group === "rank" && officerOnlyRankSubs.includes(sub) && !isOfficer) {
250 return void interaction.reply({ content: "❌ You don't have permission to use this command.", ephemeral: true });
251 }
252
253 // Route
254 if (group === "poll") {
255 if (sub === "start") return handleStart(interaction);
256 if (sub === "lock") return handleLock(interaction);
257 if (sub === "unlock") return handleUnlock(interaction);
258 if (sub === "confirm") return handleConfirm(interaction);
259 if (sub === "reload") return handleReload(interaction);
260 if (sub === "status") return handleStatus(interaction);
261 if (sub === "set-message") return handleSetMessage(interaction);
262 if (sub === "clear-message") return handleClearMessage(interaction);
263 if (sub === "set-ephemeral") return handleSetEphemeral(interaction);
264 if (sub === "clear-ephemeral") return handleClearEphemeral(interaction);
265 if (sub === "inject") return handleInject(interaction);
266 if (sub === "remove-vote") return handleRemoveVote(interaction);
267 if (sub === "purge") return handlePurge(interaction);
268 if (sub === "seed") return handleSeed(interaction);
269 }
270 if (group === "score") {
271 if (sub === "set") return handleScoreSet(interaction);
272 if (sub === "get") return handleScoreGet(interaction);
273 }
274 if (group === "rank") {
275 if (sub === "get") return handleRankGet(interaction);
276 if (sub === "post") return handleRankPost(interaction);
277 }
278 if (group === "result") {
279 if (sub === "set") return handleResultSet(interaction);
280 if (sub === "view") return handleResultView(interaction);
281 if (sub === "post") return handleResultPost(interaction);
282 }
283 if (group === "bringer") {
284 if (sub === "set") return handleBringerSet(interaction);
285 if (sub === "clear") return handleBringerClear(interaction);
286 }
287 if (group === "char") {
288 if (sub === "add") return handleCharAdd(interaction);
289 if (sub === "remove") return handleCharRemove(interaction);
290 if (sub === "set-active") return handleCharSetActive(interaction);
291 if (sub === "set-nation") return handleCharSetNation(interaction);
292 if (sub === "set-stats") return handleCharSetStats(interaction);
293 if (sub === "borrow") return handleCharBorrow(interaction);
294 if (sub === "accept") return handleCharAccept(interaction);
295 if (sub === "decline") return handleCharDecline(interaction);
296 if (sub === "share") return handleCharShare(interaction);
297 if (sub === "unshare") return handleCharUnshare(interaction);
298 if (sub === "active") return handleCharActive(interaction);
299 }
300 if (!group && sub === "switch") return handleSwitch(interaction);
301 if (!group && sub === "history") return handleHistory(interaction);
302 if (!group && sub === "impersonate") return handleImpersonate(interaction);
303}
304
305// Import char handlers here to keep tg.ts clean
306import { handleCharAdd } from "../subcommands/char/add";
307import { handleCharRemove } from "../subcommands/char/remove";
308import { handleCharSetActive } from "../subcommands/char/setActive";
309import { handleCharSetNation } from "../subcommands/char/setNation";
310import { handleCharSetStats } from "../subcommands/char/setStats";
311import { handleCharActive } from "../subcommands/char/active";
312