poll.ts
· 11 KiB · TypeScript
Eredeti
import {
EmbedBuilder,
ButtonBuilder,
ButtonStyle,
ActionRowBuilder,
TextChannel,
GuildMember,
} from "discord.js";
import { PollState, VoteEntry, Nation, TGSlot } from "@src/types";
import { cfg } from "@systems/config";
import { getEmoji, getClassEmoji, getNationEmoji } from "@systems/emojis";
import { getActiveCharacter, getCharacterByName } from "@systems/characters";
import { resolveNation } from "@systems/nations";
import { getEntry, getBringer } from "@systems/wrank";
import { nowFormatted } from "@systems/messages";
import { format } from "@format";
import { persist } from "@systems/pollPersistence"
// ─── Poll state ───────────────────────────────────────────────────────────────
export const polls: Map<number, PollState> = new Map();
const publicOverrides: Map<string, { yes?: string; no?: string }> = new Map();
const ephemeralOverrides: Map<string, { yes?: string; no?: string }> = new Map();
export function setPublicOverride(userId: string, voteType: "yes" | "no", message: string): void {
const e = publicOverrides.get(userId) ?? {};
e[voteType] = message;
publicOverrides.set(userId, e);
}
export function clearPublicOverride(userId: string, voteType?: "yes" | "no"): void {
if (!voteType) { publicOverrides.delete(userId); return; }
const e = publicOverrides.get(userId);
if (e) delete e[voteType];
}
export function setEphemeralOverride(userId: string, voteType: "yes" | "no", message: string): void {
const e = ephemeralOverrides.get(userId) ?? {};
e[voteType] = message;
ephemeralOverrides.set(userId, e);
}
export function clearEphemeralOverride(userId: string, voteType?: "yes" | "no"): void {
if (!voteType) { ephemeralOverrides.delete(userId); return; }
const e = ephemeralOverrides.get(userId);
if (e) delete e[voteType];
}
export function getPublicOverride(userId: string, voteType: "yes" | "no"): string | undefined {
return publicOverrides.get(userId)?.[voteType];
}
export function getEphemeralOverride(userId: string, voteType: "yes" | "no"): string | undefined {
return ephemeralOverrides.get(userId)?.[voteType];
}
export function resetPollOverrides(): void {
publicOverrides.clear();
ephemeralOverrides.clear();
}
export function lockPoll(slot: number): void {
const state = polls.get(slot);
if (!state) return;
state.locked = true;
// Snapshot the userKeys that were in yes at lock time
state.lockedYesKeys = new Set(
[...state.yes.values()]
.map((e) => e.userKey)
.filter((k): k is string => !!k)
);
persist.save(polls)
}
// ─── Character display ────────────────────────────────────────────────────────
function formatCharRow(entry: VoteEntry, showNationEmoji = false): string {
const cfgFormat = cfg("charDisplayFormat");
const nation = entry.characterNation;
const wRankEntry = entry.characterName ? getEntry(entry.characterName, nation ?? "Capella") : null;
let wrank = "";
if (wRankEntry) {
const wRankGoal = cfg("wRankGoal");
wrank = format.wrank.full(wRankEntry, { goal: wRankGoal, brackets: true });
}
const classStr = entry.characterClass
? (getClassEmoji(entry.characterClass) || entry.characterClass)
: "";
const levelStr = entry.characterLevel && cfg("showLevelInMessages" as any)
? `${entry.characterLevel}`
: "";
let row = cfgFormat
.replace("{wrank}", wrank)
.replace("{class}", classStr)
.replace("{level}", levelStr)
.replace("{name}", entry.characterName ?? entry.displayName)
.replace(/\s+/g, " ")
.trim();
// Bringer title — independent of W.Rank so override always shows
if (nation && entry.userKey) {
const bringer = getBringer(nation);
if (bringer && bringer === entry.characterName) {
const emoji = nation === "Capella"
? (getEmoji("luminous_bringer") || "🔆")
: (getEmoji("storm_bringer") || "⚡");
const title = nation === "Capella" ? "Luminous Bringer" : "Storm Bringer";
row += ` · ${emoji} **${title}**`;
}
}
if (entry.borrowedFrom) {
row += ` ${getEmoji("borrowed") || "🔗"}`;
}
if (showNationEmoji && nation) row = `${getNationEmoji(nation)} ${row}`;
return row;
}
// ─── Embed building ───────────────────────────────────────────────────────────
export function buildEmbed(state: PollState, overrideLockMsg?: string): EmbedBuilder {
const yesByNation = { Capella: [] as VoteEntry[], Procyon: [] as VoteEntry[] };
const noVoters: VoteEntry[] = [];
const allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[] = [];
const showNoInline = (cfg as any)("showNoInNationField") ?? false;
for (const entry of state.yes.values()) {
const nation = entry.characterNation ?? "Capella";
yesByNation[nation].push(entry);
allMessages.push({ entry, voteType: "yes" });
}
for (const entry of state.no.values()) {
noVoters.push(entry);
allMessages.push({ entry, voteType: "no" });
}
const capellaEmoji = getEmoji("capella");
const procyonEmoji = getEmoji("procyon");
const formatNationField = (nation: Nation): string => {
const yesEntries = yesByNation[nation];
const noEntries = showNoInline
? noVoters.filter((e) => e.characterNation === nation)
: [];
const lines = [
...yesEntries.map((e) => formatCharRow(e)),
...noEntries.map((e) => `❌ ${formatCharRow(e)}`),
];
return lines.length > 0 ? lines.join("\n") : "—";
};
const formatMessages = (): string => {
if (allMessages.length === 0) return "";
return allMessages
.map((m) => {
const name = m.entry.characterName ?? m.entry.displayName;
const prefix = m.voteType === "no" ? "✗ " : "✓ ";
const msg = m.entry.publicMessage ? ` — ${m.entry.publicMessage}` : "";
return `${prefix}${name} · ${m.entry.votedAt}${msg}`;
})
.join("\n");
};
const locked = state.locked;
const confirmed = state.confirmed;
const color =
confirmed === "yes" ? 0x57f287 :
confirmed === "no" ? 0xed4245 :
locked ? 0x888888 :
0xe8a317;
// Title with nation + no counts (hidden when confirmed or locked)
const counts = !locked && confirmed === null
? ` ${capellaEmoji} ${yesByNation.Capella.length} ${procyonEmoji} ${yesByNation.Procyon.length}`
: "";
const statusSuffix =
locked ? " 🔒" :
confirmed === "yes" ? " ✅" :
confirmed === "no" ? " ❌" : "";
const title = `⚔️ TG — ${state.slot}:00${counts}${statusSuffix}`;
const embed = new EmbedBuilder()
.setTitle(title)
.setColor(color)
.addFields(
{ name: `${capellaEmoji} Capella (${yesByNation.Capella.length})`, value: formatNationField("Capella"), inline: false },
{ name: "\u200b", value: "\u200b", inline: false },
{ name: `${procyonEmoji} Procyon (${yesByNation.Procyon.length})`, value: formatNationField("Procyon"), inline: false },
)
.setTimestamp();
const msgSection = formatMessages();
if (msgSection) {
embed.addFields({ name: "\u200b", value: msgSection, inline: false });
}
let footer: string;
if (confirmed === "yes") footer = cfg("confirmYesMessage");
else if (confirmed === "no") footer = cfg("confirmNoMessage");
else if (locked) footer = overrideLockMsg ?? cfg("lockMessage");
else footer = `❌ ${noVoters.length} • Vote updates live • Anyone can vote • /tg switch to change character`;
embed.setFooter({ text: footer });
return embed;
}
export function buildButtons(
disabled: boolean,
showSubmit?: boolean
): ActionRowBuilder<ButtonBuilder>[] {
if (showSubmit) {
const scoreEmoji = getEmoji("score");
const submitBtn = new ButtonBuilder()
.setCustomId("tg_score_submit")
.setLabel("Submit Score")
.setStyle(ButtonStyle.Secondary);
if (scoreEmoji) submitBtn.setEmoji(format.emoji(scoreEmoji) ?? scoreEmoji);
return [new ActionRowBuilder<ButtonBuilder>().addComponents(submitBtn)];
}
const yesBtn = new ButtonBuilder()
.setCustomId("tg_yes").setLabel("✅ Yes").setStyle(ButtonStyle.Success).setDisabled(disabled);
const noBtn = new ButtonBuilder()
.setCustomId("tg_no").setLabel("❌ No").setStyle(ButtonStyle.Danger).setDisabled(disabled);
return [new ActionRowBuilder<ButtonBuilder>().addComponents(yesBtn, noBtn)];
}
export async function updatePollMessage(
channel: TextChannel,
slot: number,
overrideLockMsg?: string,
showSubmit?: boolean
): Promise<void> {
const state = polls.get(slot);
if (!state?.messageId) return;
console.log(`[updatePollMessage] slot=${slot} showSubmit=${showSubmit} messageId=${state.messageId}`);
const buttons = buildButtons(state.locked || state.confirmed !== null, showSubmit);
console.log(`[updatePollMessage] components rows=${buttons.length}`);
try {
const msg = await channel.messages.fetch(state.messageId);
await msg.edit({ embeds: [buildEmbed(state, overrideLockMsg)], components: buttons });
} catch (err) {
console.error("Failed to update poll message:", err);
}
}
export async function postPoll(channel: TextChannel, slot: TGSlot): Promise<void> {
resetPollOverrides();
const { clearSessionBorrows } = require("@systems/borrow");
const { clearAllImpersonations } = require("@systems/impersonate");
clearSessionBorrows();
clearAllImpersonations();
const state: PollState = {
messageId: null, slot: slot.tgHour,
yes: new Map(), no: new Map(),
locked: false, confirmed: null,
};
polls.set(slot.tgHour, state);
const msg = await channel.send({ embeds: [buildEmbed(state)], components: buildButtons(false) });
state.messageId = msg.id;
console.log(`[${new Date().toISOString()}] Poll posted for ${slot.tgHour}:00.`);
}
export function createVoteEntry(
userId: string,
member: GuildMember,
userKey: string | null,
discordUsername: string
): Omit<VoteEntry, "votedAt" | "previousYesAt" | "previousNoAt" | "publicMessage"> {
const serverNickname = member.nickname ?? null;
const globalNickname = member.user.globalName ?? null;
const displayName = serverNickname ?? globalNickname ?? discordUsername;
const { getEffectiveCharacter } = require("@systems/borrow");
const { char, borrowedFrom: bf } = userKey
? getEffectiveCharacter(userKey)
: { char: null, borrowedFrom: null };
console.log(`[createVoteEntry] userKey=${userKey} char=${char?.name} borrowedFrom=${bf}`);
return {
userKey: userKey ?? (undefined as any),
displayName,
characterName: char?.name,
characterClass: char?.class,
characterLevel: char?.level,
characterNation: char?.nation ?? (resolveNation(member, userKey) ?? undefined),
borrowedFrom: bf ?? undefined,
};
}
| 1 | import { |
| 2 | EmbedBuilder, |
| 3 | ButtonBuilder, |
| 4 | ButtonStyle, |
| 5 | ActionRowBuilder, |
| 6 | TextChannel, |
| 7 | GuildMember, |
| 8 | } from "discord.js"; |
| 9 | import { PollState, VoteEntry, Nation, TGSlot } from "@src/types"; |
| 10 | import { cfg } from "@systems/config"; |
| 11 | import { getEmoji, getClassEmoji, getNationEmoji } from "@systems/emojis"; |
| 12 | import { getActiveCharacter, getCharacterByName } from "@systems/characters"; |
| 13 | import { resolveNation } from "@systems/nations"; |
| 14 | import { getEntry, getBringer } from "@systems/wrank"; |
| 15 | import { nowFormatted } from "@systems/messages"; |
| 16 | import { format } from "@format"; |
| 17 | import { persist } from "@systems/pollPersistence" |
| 18 | |
| 19 | // ─── Poll state ─────────────────────────────────────────────────────────────── |
| 20 | export const polls: Map<number, PollState> = new Map(); |
| 21 | |
| 22 | const publicOverrides: Map<string, { yes?: string; no?: string }> = new Map(); |
| 23 | const ephemeralOverrides: Map<string, { yes?: string; no?: string }> = new Map(); |
| 24 | |
| 25 | export function setPublicOverride(userId: string, voteType: "yes" | "no", message: string): void { |
| 26 | const e = publicOverrides.get(userId) ?? {}; |
| 27 | e[voteType] = message; |
| 28 | publicOverrides.set(userId, e); |
| 29 | } |
| 30 | export function clearPublicOverride(userId: string, voteType?: "yes" | "no"): void { |
| 31 | if (!voteType) { publicOverrides.delete(userId); return; } |
| 32 | const e = publicOverrides.get(userId); |
| 33 | if (e) delete e[voteType]; |
| 34 | } |
| 35 | export function setEphemeralOverride(userId: string, voteType: "yes" | "no", message: string): void { |
| 36 | const e = ephemeralOverrides.get(userId) ?? {}; |
| 37 | e[voteType] = message; |
| 38 | ephemeralOverrides.set(userId, e); |
| 39 | } |
| 40 | export function clearEphemeralOverride(userId: string, voteType?: "yes" | "no"): void { |
| 41 | if (!voteType) { ephemeralOverrides.delete(userId); return; } |
| 42 | const e = ephemeralOverrides.get(userId); |
| 43 | if (e) delete e[voteType]; |
| 44 | } |
| 45 | export function getPublicOverride(userId: string, voteType: "yes" | "no"): string | undefined { |
| 46 | return publicOverrides.get(userId)?.[voteType]; |
| 47 | } |
| 48 | export function getEphemeralOverride(userId: string, voteType: "yes" | "no"): string | undefined { |
| 49 | return ephemeralOverrides.get(userId)?.[voteType]; |
| 50 | } |
| 51 | export function resetPollOverrides(): void { |
| 52 | publicOverrides.clear(); |
| 53 | ephemeralOverrides.clear(); |
| 54 | } |
| 55 | |
| 56 | export function lockPoll(slot: number): void { |
| 57 | const state = polls.get(slot); |
| 58 | if (!state) return; |
| 59 | state.locked = true; |
| 60 | |
| 61 | // Snapshot the userKeys that were in yes at lock time |
| 62 | state.lockedYesKeys = new Set( |
| 63 | [...state.yes.values()] |
| 64 | .map((e) => e.userKey) |
| 65 | .filter((k): k is string => !!k) |
| 66 | ); |
| 67 | |
| 68 | persist.save(polls) |
| 69 | } |
| 70 | |
| 71 | |
| 72 | // ─── Character display ──────────────────────────────────────────────────────── |
| 73 | function formatCharRow(entry: VoteEntry, showNationEmoji = false): string { |
| 74 | const cfgFormat = cfg("charDisplayFormat"); |
| 75 | const nation = entry.characterNation; |
| 76 | const wRankEntry = entry.characterName ? getEntry(entry.characterName, nation ?? "Capella") : null; |
| 77 | |
| 78 | let wrank = ""; |
| 79 | if (wRankEntry) { |
| 80 | const wRankGoal = cfg("wRankGoal"); |
| 81 | wrank = format.wrank.full(wRankEntry, { goal: wRankGoal, brackets: true }); |
| 82 | } |
| 83 | |
| 84 | const classStr = entry.characterClass |
| 85 | ? (getClassEmoji(entry.characterClass) || entry.characterClass) |
| 86 | : ""; |
| 87 | |
| 88 | const levelStr = entry.characterLevel && cfg("showLevelInMessages" as any) |
| 89 | ? `${entry.characterLevel}` |
| 90 | : ""; |
| 91 | |
| 92 | let row = cfgFormat |
| 93 | .replace("{wrank}", wrank) |
| 94 | .replace("{class}", classStr) |
| 95 | .replace("{level}", levelStr) |
| 96 | .replace("{name}", entry.characterName ?? entry.displayName) |
| 97 | .replace(/\s+/g, " ") |
| 98 | .trim(); |
| 99 | |
| 100 | // Bringer title — independent of W.Rank so override always shows |
| 101 | if (nation && entry.userKey) { |
| 102 | const bringer = getBringer(nation); |
| 103 | if (bringer && bringer === entry.characterName) { |
| 104 | const emoji = nation === "Capella" |
| 105 | ? (getEmoji("luminous_bringer") || "🔆") |
| 106 | : (getEmoji("storm_bringer") || "⚡"); |
| 107 | const title = nation === "Capella" ? "Luminous Bringer" : "Storm Bringer"; |
| 108 | row += ` · ${emoji} **${title}**`; |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | if (entry.borrowedFrom) { |
| 113 | row += ` ${getEmoji("borrowed") || "🔗"}`; |
| 114 | } |
| 115 | |
| 116 | if (showNationEmoji && nation) row = `${getNationEmoji(nation)} ${row}`; |
| 117 | |
| 118 | return row; |
| 119 | } |
| 120 | |
| 121 | // ─── Embed building ─────────────────────────────────────────────────────────── |
| 122 | export function buildEmbed(state: PollState, overrideLockMsg?: string): EmbedBuilder { |
| 123 | const yesByNation = { Capella: [] as VoteEntry[], Procyon: [] as VoteEntry[] }; |
| 124 | const noVoters: VoteEntry[] = []; |
| 125 | const allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[] = []; |
| 126 | const showNoInline = (cfg as any)("showNoInNationField") ?? false; |
| 127 | |
| 128 | for (const entry of state.yes.values()) { |
| 129 | const nation = entry.characterNation ?? "Capella"; |
| 130 | yesByNation[nation].push(entry); |
| 131 | allMessages.push({ entry, voteType: "yes" }); |
| 132 | } |
| 133 | for (const entry of state.no.values()) { |
| 134 | noVoters.push(entry); |
| 135 | allMessages.push({ entry, voteType: "no" }); |
| 136 | } |
| 137 | |
| 138 | const capellaEmoji = getEmoji("capella"); |
| 139 | const procyonEmoji = getEmoji("procyon"); |
| 140 | |
| 141 | const formatNationField = (nation: Nation): string => { |
| 142 | const yesEntries = yesByNation[nation]; |
| 143 | const noEntries = showNoInline |
| 144 | ? noVoters.filter((e) => e.characterNation === nation) |
| 145 | : []; |
| 146 | const lines = [ |
| 147 | ...yesEntries.map((e) => formatCharRow(e)), |
| 148 | ...noEntries.map((e) => `❌ ${formatCharRow(e)}`), |
| 149 | ]; |
| 150 | return lines.length > 0 ? lines.join("\n") : "—"; |
| 151 | }; |
| 152 | |
| 153 | const formatMessages = (): string => { |
| 154 | if (allMessages.length === 0) return ""; |
| 155 | return allMessages |
| 156 | .map((m) => { |
| 157 | const name = m.entry.characterName ?? m.entry.displayName; |
| 158 | const prefix = m.voteType === "no" ? "✗ " : "✓ "; |
| 159 | const msg = m.entry.publicMessage ? ` — ${m.entry.publicMessage}` : ""; |
| 160 | return `${prefix}${name} · ${m.entry.votedAt}${msg}`; |
| 161 | }) |
| 162 | .join("\n"); |
| 163 | }; |
| 164 | |
| 165 | const locked = state.locked; |
| 166 | const confirmed = state.confirmed; |
| 167 | |
| 168 | const color = |
| 169 | confirmed === "yes" ? 0x57f287 : |
| 170 | confirmed === "no" ? 0xed4245 : |
| 171 | locked ? 0x888888 : |
| 172 | 0xe8a317; |
| 173 | |
| 174 | // Title with nation + no counts (hidden when confirmed or locked) |
| 175 | const counts = !locked && confirmed === null |
| 176 | ? ` ${capellaEmoji} ${yesByNation.Capella.length} ${procyonEmoji} ${yesByNation.Procyon.length}` |
| 177 | : ""; |
| 178 | const statusSuffix = |
| 179 | locked ? " 🔒" : |
| 180 | confirmed === "yes" ? " ✅" : |
| 181 | confirmed === "no" ? " ❌" : ""; |
| 182 | |
| 183 | const title = `⚔️ TG — ${state.slot}:00${counts}${statusSuffix}`; |
| 184 | |
| 185 | const embed = new EmbedBuilder() |
| 186 | .setTitle(title) |
| 187 | .setColor(color) |
| 188 | .addFields( |
| 189 | { name: `${capellaEmoji} Capella (${yesByNation.Capella.length})`, value: formatNationField("Capella"), inline: false }, |
| 190 | { name: "\u200b", value: "\u200b", inline: false }, |
| 191 | { name: `${procyonEmoji} Procyon (${yesByNation.Procyon.length})`, value: formatNationField("Procyon"), inline: false }, |
| 192 | ) |
| 193 | .setTimestamp(); |
| 194 | |
| 195 | const msgSection = formatMessages(); |
| 196 | if (msgSection) { |
| 197 | embed.addFields({ name: "\u200b", value: msgSection, inline: false }); |
| 198 | } |
| 199 | |
| 200 | let footer: string; |
| 201 | if (confirmed === "yes") footer = cfg("confirmYesMessage"); |
| 202 | else if (confirmed === "no") footer = cfg("confirmNoMessage"); |
| 203 | else if (locked) footer = overrideLockMsg ?? cfg("lockMessage"); |
| 204 | else footer = `❌ ${noVoters.length} • Vote updates live • Anyone can vote • /tg switch to change character`; |
| 205 | embed.setFooter({ text: footer }); |
| 206 | |
| 207 | return embed; |
| 208 | } |
| 209 | |
| 210 | export function buildButtons( |
| 211 | disabled: boolean, |
| 212 | showSubmit?: boolean |
| 213 | ): ActionRowBuilder<ButtonBuilder>[] { |
| 214 | if (showSubmit) { |
| 215 | const scoreEmoji = getEmoji("score"); |
| 216 | const submitBtn = new ButtonBuilder() |
| 217 | .setCustomId("tg_score_submit") |
| 218 | .setLabel("Submit Score") |
| 219 | .setStyle(ButtonStyle.Secondary); |
| 220 | if (scoreEmoji) submitBtn.setEmoji(format.emoji(scoreEmoji) ?? scoreEmoji); |
| 221 | return [new ActionRowBuilder<ButtonBuilder>().addComponents(submitBtn)]; |
| 222 | } |
| 223 | |
| 224 | const yesBtn = new ButtonBuilder() |
| 225 | .setCustomId("tg_yes").setLabel("✅ Yes").setStyle(ButtonStyle.Success).setDisabled(disabled); |
| 226 | const noBtn = new ButtonBuilder() |
| 227 | .setCustomId("tg_no").setLabel("❌ No").setStyle(ButtonStyle.Danger).setDisabled(disabled); |
| 228 | return [new ActionRowBuilder<ButtonBuilder>().addComponents(yesBtn, noBtn)]; |
| 229 | } |
| 230 | |
| 231 | export async function updatePollMessage( |
| 232 | channel: TextChannel, |
| 233 | slot: number, |
| 234 | overrideLockMsg?: string, |
| 235 | showSubmit?: boolean |
| 236 | ): Promise<void> { |
| 237 | const state = polls.get(slot); |
| 238 | if (!state?.messageId) return; |
| 239 | console.log(`[updatePollMessage] slot=${slot} showSubmit=${showSubmit} messageId=${state.messageId}`); |
| 240 | const buttons = buildButtons(state.locked || state.confirmed !== null, showSubmit); |
| 241 | console.log(`[updatePollMessage] components rows=${buttons.length}`); |
| 242 | try { |
| 243 | const msg = await channel.messages.fetch(state.messageId); |
| 244 | await msg.edit({ embeds: [buildEmbed(state, overrideLockMsg)], components: buttons }); |
| 245 | } catch (err) { |
| 246 | console.error("Failed to update poll message:", err); |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | export async function postPoll(channel: TextChannel, slot: TGSlot): Promise<void> { |
| 251 | resetPollOverrides(); |
| 252 | const { clearSessionBorrows } = require("@systems/borrow"); |
| 253 | const { clearAllImpersonations } = require("@systems/impersonate"); |
| 254 | |
| 255 | clearSessionBorrows(); |
| 256 | clearAllImpersonations(); |
| 257 | |
| 258 | const state: PollState = { |
| 259 | messageId: null, slot: slot.tgHour, |
| 260 | yes: new Map(), no: new Map(), |
| 261 | locked: false, confirmed: null, |
| 262 | }; |
| 263 | polls.set(slot.tgHour, state); |
| 264 | const msg = await channel.send({ embeds: [buildEmbed(state)], components: buildButtons(false) }); |
| 265 | state.messageId = msg.id; |
| 266 | console.log(`[${new Date().toISOString()}] Poll posted for ${slot.tgHour}:00.`); |
| 267 | } |
| 268 | |
| 269 | export function createVoteEntry( |
| 270 | userId: string, |
| 271 | member: GuildMember, |
| 272 | userKey: string | null, |
| 273 | discordUsername: string |
| 274 | ): Omit<VoteEntry, "votedAt" | "previousYesAt" | "previousNoAt" | "publicMessage"> { |
| 275 | const serverNickname = member.nickname ?? null; |
| 276 | const globalNickname = member.user.globalName ?? null; |
| 277 | const displayName = serverNickname ?? globalNickname ?? discordUsername; |
| 278 | |
| 279 | const { getEffectiveCharacter } = require("@systems/borrow"); |
| 280 | const { char, borrowedFrom: bf } = userKey |
| 281 | ? getEffectiveCharacter(userKey) |
| 282 | : { char: null, borrowedFrom: null }; |
| 283 | console.log(`[createVoteEntry] userKey=${userKey} char=${char?.name} borrowedFrom=${bf}`); |
| 284 | |
| 285 | return { |
| 286 | userKey: userKey ?? (undefined as any), |
| 287 | displayName, |
| 288 | characterName: char?.name, |
| 289 | characterClass: char?.class, |
| 290 | characterLevel: char?.level, |
| 291 | characterNation: char?.nation ?? (resolveNation(member, userKey) ?? undefined), |
| 292 | borrowedFrom: bf ?? undefined, |
| 293 | }; |
| 294 | } |