import { Client, TextChannel, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; import fs from "fs"; import path from "path"; import { BorrowRequest } from "@src/types"; import { cfg } from "@systems/config"; import { getCharacterByName } from "@systems/characters"; const PREFS_PATH = path.join(__dirname, "../../data/sessionPreferences.json"); // ─── Persistent preferences ─────────────────────────────────────────────────── let _prefs: Record = {}; function loadPrefs(): void { try { _prefs = JSON.parse(fs.readFileSync(PREFS_PATH, "utf8")); } catch { _prefs = {}; } } function savePrefs(): void { try { fs.writeFileSync(PREFS_PATH, JSON.stringify(_prefs, null, 2)); } catch (err) { console.error("Failed to save sessionPreferences.json:", err); } } loadPrefs(); export function setPersistentPreference(userKey: string, ownerKey: string, charName: string): void { _prefs[userKey] = { ownerKey, charName }; savePrefs(); } export function clearPersistentPreference(userKey: string): void { delete _prefs[userKey]; savePrefs(); } export function getPersistentPreference(userKey: string): { ownerKey: string; charName: string } | null { return _prefs[userKey] ?? null; } // ─── Active borrow requests ─────────────────────────────────────────────────── const pendingRequests: Map = new Map(); const sessionBorrows: Map = new Map(); const borrowDmMessages: Map = new Map(); function requestKey(ownerKey: string, requesterKey: string): string { return `${ownerKey}:${requesterKey}`; } export function getPendingRequest(ownerKey: string, requesterKey: string): BorrowRequest | null { return pendingRequests.get(requestKey(ownerKey, requesterKey)) ?? null; } export function getPendingRequestByKey(key: string): BorrowRequest | null { return pendingRequests.get(key) ?? null; } export function getAllPendingForOwner(ownerKey: string): BorrowRequest[] { return [...pendingRequests.values()].filter((r) => r.ownerKey === ownerKey); } export function addPendingRequest(request: BorrowRequest): void { const key = requestKey(request.ownerKey, request.requesterKey); const expiry = cfg("borrowRequestExpiryMs" as any) ?? 0; pendingRequests.set(key, request); if (expiry > 0) { setTimeout(() => { if (pendingRequests.get(key)?.requestedAt === request.requestedAt) { pendingRequests.delete(key); console.log(`[borrow] Request ${key} expired.`); } }, expiry); } } export function removePendingRequest(ownerKey: string, requesterKey: string): void { pendingRequests.delete(requestKey(ownerKey, requesterKey)); } export function storeDmMessage(ownerKey: string, requesterKey: string, channelId: string, messageId: string): void { borrowDmMessages.set(requestKey(ownerKey, requesterKey), { channelId, messageId }); } export function getDmMessage(ownerKey: string, requesterKey: string): { channelId: string; messageId: string } | null { return borrowDmMessages.get(requestKey(ownerKey, requesterKey)) ?? null; } // ─── Session borrows ────────────────────────────────────────────────────────── export function setSessionBorrow(requesterKey: string, ownerKey: string, charName: string): void { sessionBorrows.set(requesterKey, { ownerKey, charName }); } export function getSessionBorrow(requesterKey: string): { ownerKey: string; charName: string } | null { return sessionBorrows.get(requesterKey) ?? null; } export function clearSessionBorrows(): void { sessionBorrows.clear(); borrowDmMessages.clear(); } export function canUseCharacter(requesterKey: string, ownerKey: string, charName: string): boolean { if (requesterKey === ownerKey) return true; const char = getCharacterByName(ownerKey, charName); if (char?.sharedWith?.includes(requesterKey)) return true; const borrow = getSessionBorrow(requesterKey); if (borrow && borrow.ownerKey === ownerKey && borrow.charName.toLowerCase() === charName.toLowerCase()) return true; return false; } export function clearSessionBorrowForUser(userKey: string): void { sessionBorrows.delete(userKey); } // ─── DM notifications ───────────────────────────────────────────────────────── export async function sendBorrowRequestDM( client: Client, ownerDiscordId: string, requesterDisplayName: string, ownerKey: string, requesterKey: string, charName: string, charClass: string, charLevel: number, fallbackChannel?: TextChannel ): Promise { const content = `🔔 **Borrow Request**\n**${requesterDisplayName}** wants to borrow **${charName}** (${charClass} · Lv${charLevel}) for tonight's TG.`; const acceptBtn = new ButtonBuilder() .setCustomId(`borrow_accept:${ownerKey}:${requesterKey}`) .setLabel("✅ Accept") .setStyle(ButtonStyle.Success); const declineBtn = new ButtonBuilder() .setCustomId(`borrow_decline:${ownerKey}:${requesterKey}`) .setLabel("❌ Decline") .setStyle(ButtonStyle.Danger); const row = new ActionRowBuilder().addComponents(acceptBtn, declineBtn); try { const ownerUser = await client.users.fetch(ownerDiscordId); const dm = await ownerUser.createDM(); const msg = await dm.send({ content, components: [row] }); storeDmMessage(ownerKey, requesterKey, dm.id, msg.id); } catch { if (fallbackChannel) { await fallbackChannel.send({ content: `<@${ownerDiscordId}> ${content}\nUse \`/tg char accept ${requesterKey}\` or \`/tg char decline ${requesterKey}\`.`, }); } } } export async function updateBorrowDM( client: Client, ownerKey: string, requesterKey: string, accepted: boolean ): Promise { const dm = getDmMessage(ownerKey, requesterKey); if (!dm) return; try { const channel = await client.channels.fetch(dm.channelId) as any; const message = await channel.messages.fetch(dm.messageId); const status = accepted ? "✅ Accepted" : "❌ Declined"; await message.edit({ content: `${message.content}\n\n*${status}*`, components: [] }); } catch {} } // ─── Effective character resolution ────────────────────────────────────────── export function getEffectiveCharacter(userKey: string): { char: any; borrowedFrom: string | null } { const { getActiveCharacter, getCharacterByName: getChar } = require("./characters"); // 1. Session borrow (temporary, resets on poll start) const borrow = getSessionBorrow(userKey); if (borrow) { const char = getChar(borrow.ownerKey, borrow.charName); if (char) return { char, borrowedFrom: borrow.ownerKey }; } // 2. Persistent preference (survives restarts and poll resets) const pref = getPersistentPreference(userKey); console.log(`[getEffectiveCharacter] userKey=${userKey} sessionBorrow=${JSON.stringify(borrow)} pref=${JSON.stringify(pref)}`); if (pref) { const char = getChar(pref.ownerKey, pref.charName); if (char) return { char, borrowedFrom: pref.ownerKey }; clearPersistentPreference(userKey); } // 3. Own active character const char = getActiveCharacter(userKey); return { char: char ?? null, borrowedFrom: null }; }