- /tg-admin result post-all [slot:] — posts/updates every TG result for a slot (default 20:00) in one pass, not one at a time - Officers can add a late-arriving player to a poll (locked or not) so they can submit a score instead of hitting "You weren't in this TG": /tg poll add-attendee (single, autocomplete) and /tg-admin poll add-attendees (modal, comma/newline-separated, for adding several at once — Discord modals can't hold select menus, only text inputs) - fix: /tg-admin result post, leaderboard post, and leaderboard post-highlights had no permission check at all — any guild member could invoke them - fix: --register never exited after registering slash commands, so it kept running as a full second bot instance forever. Every docker exec --register run stacked another live process on top of the container's real one, each independently connected to Discord — root cause of /tg poll start posting multiple polls and vote/submit state acting confused (confirmed 3 live processes via docker top, cleaned up) - fix: bot startup poll-restore logic had the same stale-button- resurrection bug already fixed in /tg poll reload, in a sibling code path that got missed — now respects scoreSubmitOpen/buttonsRemoved there too instead of re-deriving from locked/confirmed - rename: officer -> moderator throughout (Config.roles, hasOfficerRole, all "officer only" text, /tg-config roles set/add/remove/reset-* commands). Config.load() auto-migrates a legacy roles.officer key on read so existing deployed config.json files don't silently reset. Slash command names changed — re-register after deploying. - data/updates v0.10.3 changelog for the above
78 lines
No EOL
3 KiB
TypeScript
78 lines
No EOL
3 KiB
TypeScript
import { ChatInputCommandInteraction } from "discord.js";
|
|
import { Config } from "@systems/config";
|
|
import { CharacterRegistry } from "@registry/character-registry";
|
|
import { Score } from "@systems/score";
|
|
import { TGKey } from "@systems/tg-key";
|
|
import { Discord } from "@discord";
|
|
import { RuntimeEvents } from "@systems/runtime";
|
|
import { hasModeratorRole } from "@systems/users";
|
|
import { parseStatValue } from "@helpers/stat-value";
|
|
|
|
export async function handleScoreInject(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
await Discord.Interaction.deferReply(interaction, { ephemeral: true });
|
|
|
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
|
if (!hasModeratorRole(member, Config.get({ section: "roles", key: "moderator" }))) {
|
|
await Discord.Interaction.editReply(interaction, "❌ Moderator only.");
|
|
return;
|
|
}
|
|
|
|
const opts = Discord.Interaction.options(interaction);
|
|
const charName = opts.string({ key: "char_name", required: true })!;
|
|
const playedByArg = opts.string({ key: "played_by" }) ?? undefined;
|
|
const pts = opts.integer({ key: "pts", required: true })!;
|
|
const slot = opts.integer({ key: "slot", required: true })!;
|
|
const date = opts.string({ key: "date" }) ?? new Date().toISOString().slice(0, 10);
|
|
const k = opts.integer({ key: "k" }) ?? undefined;
|
|
const d = opts.integer({ key: "d" }) ?? undefined;
|
|
const atkRaw = opts.string({ key: "atk" });
|
|
const defRaw = opts.string({ key: "def" });
|
|
const healRaw = opts.string({ key: "heal" });
|
|
const atkResult = parseStatValue(atkRaw, "Attack score");
|
|
const defResult = parseStatValue(defRaw, "Defense score");
|
|
const healResult = parseStatValue(healRaw, "Healing score");
|
|
const atk = atkResult.value;
|
|
const def = defResult.value;
|
|
const heal = healResult.value;
|
|
|
|
const statError = atkResult.error ?? defResult.error ?? healResult.error;
|
|
if (statError) {
|
|
await Discord.Interaction.editReply(interaction, `❌ ${statError}`);
|
|
return;
|
|
}
|
|
|
|
const char = CharacterRegistry.find(charName);
|
|
if (!char) {
|
|
await Discord.Interaction.editReply(interaction, `❌ Character **${charName}** not found.`);
|
|
return;
|
|
}
|
|
|
|
const historyKey = TGKey.from({ date: date, slot });
|
|
|
|
await Score.submit({
|
|
character: char,
|
|
pts,
|
|
k,
|
|
d,
|
|
atk,
|
|
def,
|
|
heal,
|
|
slot,
|
|
date,
|
|
playedBy: playedByArg,
|
|
submittedByModerator: true,
|
|
});
|
|
|
|
await RuntimeEvents.emit("scoreSubmitted", { historyKey, character: char });
|
|
await Discord.Interaction.editReply(interaction,
|
|
`✅ Score injected for **${char.name}** — 📊 ${pts}${k !== undefined ? ` ⚔️ ${k}/${d ?? 0}` : ""} · \`${TGKey.toDisplay(historyKey)}\``
|
|
);
|
|
}
|
|
|
|
export const ScoreInjectCommands = {
|
|
inject: handleScoreInject,
|
|
autocompleteHistory: async (interaction: any) => {
|
|
const { autocompleteHistoryKey } = require("./result-post");
|
|
await autocompleteHistoryKey(interaction);
|
|
},
|
|
}; |