109 lines
No EOL
4 KiB
TypeScript
109 lines
No EOL
4 KiB
TypeScript
import { ButtonInteraction, TextChannel } from "discord.js";
|
|
import { cfg } from "../systems/config";
|
|
import { resolveUser } from "../systems/users";
|
|
import { resolveMessage, nowFormatted } from "../systems/messages";
|
|
import { resolveNation } from "../systems/nations";
|
|
import { polls, updatePollMessage, createVoteEntry, getPublicOverride, getEphemeralOverride } from "../systems/poll";
|
|
|
|
const EPHEMERAL_ENABLED = process.env.EPHEMERAL_ENABLED !== "false";
|
|
const EPHEMERAL_DELETE_MS = parseInt(process.env.EPHEMERAL_DELETE_MS ?? "5000");
|
|
const LOCK_AT = parseInt(process.env.LOCK_AT ?? "10");
|
|
|
|
const clickCounts = new Map<string, { yes: number; no: number }>();
|
|
|
|
export async function handleButton(interaction: ButtonInteraction): Promise<void> {
|
|
if (!["tg_yes", "tg_no"].includes(interaction.customId)) return;
|
|
|
|
// Defer immediately to avoid 3s timeout
|
|
await interaction.deferUpdate();
|
|
|
|
const slot = [...polls.entries()].find(([, s]) => s.messageId === interaction.message.id)?.[0];
|
|
if (slot === undefined) return;
|
|
|
|
const state = polls.get(slot)!;
|
|
if (state.locked || state.confirmed !== null) return;
|
|
|
|
const userId = interaction.user.id;
|
|
const member = await interaction.guild!.members.fetch(userId);
|
|
const user = await resolveUser(member);
|
|
const votedYes = interaction.customId === "tg_yes";
|
|
const now = nowFormatted();
|
|
|
|
// Check nation — block if no nation
|
|
const nation = resolveNation(member, user.usermapKey);
|
|
if (!nation) {
|
|
if (EPHEMERAL_ENABLED) {
|
|
const reply = await interaction.followUp({ content: "❌ You must be in Capella or Procyon to vote.", ephemeral: true });
|
|
if (EPHEMERAL_DELETE_MS > 0) setTimeout(() => reply.delete().catch(() => {}), EPHEMERAL_DELETE_MS);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Click tracking
|
|
if (!clickCounts.has(userId)) clickCounts.set(userId, { yes: 0, no: 0 });
|
|
const clicks = clickCounts.get(userId)!;
|
|
|
|
if (votedYes && clicks.yes >= LOCK_AT) return;
|
|
if (!votedYes && clicks.no >= LOCK_AT) return;
|
|
|
|
// Ignore same vote
|
|
if (votedYes && state.yes.has(userId)) return;
|
|
if (!votedYes && state.no.has(userId)) return;
|
|
|
|
if (votedYes) clicks.yes += 1;
|
|
else clicks.no += 1;
|
|
|
|
const clickCount = votedYes ? clicks.yes : clicks.no;
|
|
|
|
// Resolve messages — officer override takes priority
|
|
const publicMsg = getPublicOverride(userId, votedYes ? "yes" : "no")
|
|
?? resolveMessage("public", votedYes ? "yes" : "no", clickCount, user.discordUsername, user.serverNickname, user.globalNickname);
|
|
|
|
const ephemeralMsg = getEphemeralOverride(userId, votedYes ? "yes" : "no")
|
|
?? resolveMessage("ephemeral", votedYes ? "yes" : "no", clickCount, user.discordUsername, user.serverNickname, user.globalNickname);
|
|
|
|
const baseEntry = createVoteEntry(userId, member, user.usermapKey, user.discordUsername);
|
|
|
|
if (votedYes) {
|
|
const previousNo = state.no.get(userId);
|
|
state.no.delete(userId);
|
|
state.yes.set(userId, {
|
|
...baseEntry,
|
|
votedAt: now,
|
|
previousNoAt: previousNo?.votedAt,
|
|
publicMessage: publicMsg ?? undefined,
|
|
});
|
|
} else {
|
|
const previousYes = state.yes.get(userId);
|
|
state.yes.delete(userId);
|
|
state.no.set(userId, {
|
|
...baseEntry,
|
|
votedAt: now,
|
|
previousYesAt: previousYes?.votedAt,
|
|
publicMessage: publicMsg ?? undefined,
|
|
});
|
|
}
|
|
|
|
const locked = clickCount >= LOCK_AT;
|
|
if (locked) state.locked = true;
|
|
|
|
// Send ephemeral follow-up (since we already deferred with deferUpdate)
|
|
if (EPHEMERAL_ENABLED) {
|
|
const lockedSuffix = locked ? "\n🔒 *You've been locked in.*" : "";
|
|
const content = ephemeralMsg
|
|
? `${ephemeralMsg}${lockedSuffix}`
|
|
: locked ? "🔒 You've been locked in." : null;
|
|
|
|
if (content) {
|
|
const reply = await interaction.followUp({ content, ephemeral: true });
|
|
if (EPHEMERAL_DELETE_MS > 0) setTimeout(() => reply.delete().catch(() => {}), EPHEMERAL_DELETE_MS);
|
|
}
|
|
}
|
|
|
|
const channel = interaction.channel as TextChannel;
|
|
await updatePollMessage(channel, slot);
|
|
}
|
|
|
|
export function resetClickCounts(): void {
|
|
clickCounts.clear();
|
|
} |