tg-bot-ts/src/subcommands/admin/score-inject.ts
Nuno Duque Nunes 4848b37028 feature: bulk result posting, late-attendee handling, and officer->moderator rename
- /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
2026-07-31 03:55:37 +01:00

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);
},
};