tg-bot-ts/src/subcommands/admin/sleep-check.ts
Nuno Duque Nunes fc4f322885 feature: shorthand stat input, admin score tools, and sleep check system
- ATK/DEF/Heal now accept K/M shorthand (500K, 1.4M, case-insensitive) via
  a shared parser (@helpers/stat-value), used identically by the Submit
  Score modal, /tg score set, and /tg-admin score-inject
- /tg-admin score-modal — officers can open the Submit Score modal on
  behalf of any player; the modal's customId now carries the target
  userKey so submission always resolves to the intended player rather
  than whoever's Discord client is submitting it
- fix: /tg call now actually reveals the Submit Score button (was
  recording the call but never opening submission)
- fix: /tg poll reload no longer resurrects Submit Score / Yes-No buttons
  that had already been removed — poll state now tracks submission-open
  and buttons-removed explicitly instead of re-deriving it from
  locked/confirmed on every render
- fix: midnight cleanup now fully removes all poll buttons (not just
  Submit Score) instead of leaving them disabled
- Sleep Check — players on a configured role get flagged (💤 shown on
  their poll row) the moment they vote Yes; a scheduled per-slot job DMs
  everyone currently flagged at a configurable time before TG (default
  20 min). State changes and DM sending are intentionally decoupled —
  flagging never itself sends a message. Officers can also flag/clear
  manually via /tg-admin sleep-check set|clear
- data/updates/v0.9.2 and new v0.10 changelog entries for the above
- merged the two .claude reference docs into one, removed stale/fixed
  items, documented the new systems and known deferred gaps
2026-07-30 04:16:42 +01:00

80 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ChatInputCommandInteraction, TextChannel } from "discord.js";
import { Config } from "@systems/config";
import { hasOfficerRole } from "@systems/users";
import { polls, updatePollMessage } from "@systems/poll";
import { persist } from "@systems/pollPersistence";
import { VoteEntry, PollState } from "@types";
import { Discord } from "@discord";
import { replyAndDelete } from "@utils";
interface FoundEntry {
slot: number;
state: PollState;
entry: VoteEntry;
}
function findYesEntry(userKey: string): FoundEntry | null {
const slot = [...polls.keys()][0];
if (slot === undefined) return null;
const state = polls.get(slot)!;
const entry = [...state.yes.values()].find((e) => e.userKey === userKey);
return entry ? { slot, state, entry } : null;
}
export async function handleSleepCheckSet(interaction: ChatInputCommandInteraction): Promise<void> {
const member = await interaction.guild!.members.fetch(interaction.user.id);
if (!hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }))) {
return void replyAndDelete(interaction, "❌ Officer only.", true);
}
const opts = Discord.Interaction.options(interaction);
const userKey = opts.string({ key: "name", required: true })!;
const found = findYesEntry(userKey);
if (!found) {
return void replyAndDelete(interaction, `❌ **${userKey}** hasn't voted Yes in the active poll.`, true);
}
found.entry.sleepCheckPending = true;
persist.save(polls);
const channel = await interaction.client.channels.fetch(
Config.get({ section: "channels", key: "poll" })
) as TextChannel;
await updatePollMessage(channel, found.slot, undefined, found.state.scoreSubmitOpen === true);
return void replyAndDelete(interaction, `💤 **${userKey}** flagged for sleep check. The confirmation DM goes out at the usual scheduled time.`, true);
}
export async function handleSleepCheckClear(interaction: ChatInputCommandInteraction): Promise<void> {
const member = await interaction.guild!.members.fetch(interaction.user.id);
if (!hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }))) {
return void replyAndDelete(interaction, "❌ Officer only.", true);
}
const opts = Discord.Interaction.options(interaction);
const userKey = opts.string({ key: "name", required: true })!;
const found = findYesEntry(userKey);
if (!found) {
return void replyAndDelete(interaction, `❌ **${userKey}** hasn't voted Yes in the active poll.`, true);
}
if (!found.entry.sleepCheckPending) {
return void replyAndDelete(interaction, ` **${userKey}** doesn't have a pending sleep check.`, true);
}
found.entry.sleepCheckPending = false;
persist.save(polls);
const channel = await interaction.client.channels.fetch(
Config.get({ section: "channels", key: "poll" })
) as TextChannel;
await updatePollMessage(channel, found.slot, undefined, found.state.scoreSubmitOpen === true);
return void replyAndDelete(interaction, `✅ Sleep check cleared for **${userKey}**.`, true);
}
export const SleepCheckCommands = {
set: handleSleepCheckSet,
clear: handleSleepCheckClear,
};