import { Client, TextChannel } from "discord.js"; import { BorrowRequest } from "../types"; import { cfg } from "./config"; import { getCharacterByName } from "./characters"; // Active borrow requests (pending accept/decline) const pendingRequests: Map = new Map(); // key: `${ownerKey}:${requesterKey}` // Session borrows: usermapKey โ†’ { ownerKey, charName } โ€” reset on poll start const sessionBorrows: Map = new Map(); // DM message IDs for updating borrow request messages 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 borrow management 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(); } // Check if a user can use a character (owns it or has share/borrow access) export function canUseCharacter(requesterKey: string, ownerKey: string, charName: string): boolean { if (requesterKey === ownerKey) return true; // Check persistent share const char = getCharacterByName(ownerKey, charName); if (char?.sharedWith?.includes(requesterKey)) return true; // Check session borrow const borrow = getSessionBorrow(requesterKey); if (borrow && borrow.ownerKey === ownerKey && borrow.charName.toLowerCase() === charName.toLowerCase()) return true; return false; } // Send borrow request DM to owner, fall back to poll channel ephemeral export async function sendBorrowRequestDM( client: Client, ownerDiscordId: string, requesterDisplayName: string, ownerKey: string, requesterKey: string, charName: string, charClass: string, charLevel: number, fallbackChannel?: TextChannel ): Promise { const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = await import("discord.js"); 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 { // DM failed โ€” fall back to poll channel ephemeral if (fallbackChannel) { await fallbackChannel.send({ content: `<@${ownerDiscordId}> ${content}\nUse \`/tg char accept ${requesterKey}\` or \`/tg char decline ${requesterKey}\`.`, }); } } } // Update DM after accept/decline to disable buttons 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 { // DM may have been deleted, ignore } } // Returns the effective active character for a user // Priority: session borrow โ†’ persistent preference โ†’ own active character export function getEffectiveCharacter(usermapKey: string): { char: any; borrowedFrom: string | null } { const { getActiveCharacter, getCharacterByName } = require("./characters"); // 1. Session borrow (temporary, resets on poll start) const borrow = getSessionBorrow(usermapKey); if (borrow) { const char = getCharacterByName(borrow.ownerKey, borrow.charName); if (char) return { char, borrowedFrom: borrow.ownerKey }; } // 2. Persistent preference (set when switching to a shared char, survives restarts) const pref = getPersistentPreference(usermapKey); if (pref) { const char = getCharacterByName(pref.ownerKey, pref.charName); if (char) return { char, borrowedFrom: pref.ownerKey }; // Char no longer exists or share was revoked โ€” clear preference clearPersistentPreference(usermapKey); } // 3. Own active character const char = getActiveCharacter(usermapKey); return { char: char ?? null, borrowedFrom: null }; }