Last active 3 weeks ago

nuno revised this gist 3 weeks ago. Go to revision

1 file changed, 192 insertions

gistfile1.txt(file created)

@@ -0,0 +1,192 @@
1 + import { Client, TextChannel, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
2 + import fs from "fs";
3 + import path from "path";
4 + import { BorrowRequest } from "@src/types";
5 + import { cfg } from "@systems/config";
6 + import { getCharacterByName } from "@systems/characters";
7 +
8 + const PREFS_PATH = path.join(__dirname, "../../data/sessionPreferences.json");
9 +
10 + // ─── Persistent preferences ───────────────────────────────────────────────────
11 + let _prefs: Record<string, { ownerKey: string; charName: string }> = {};
12 +
13 + function loadPrefs(): void {
14 + try { _prefs = JSON.parse(fs.readFileSync(PREFS_PATH, "utf8")); }
15 + catch { _prefs = {}; }
16 + }
17 +
18 + function savePrefs(): void {
19 + try { fs.writeFileSync(PREFS_PATH, JSON.stringify(_prefs, null, 2)); }
20 + catch (err) { console.error("Failed to save sessionPreferences.json:", err); }
21 + }
22 +
23 + loadPrefs();
24 +
25 + export function setPersistentPreference(userKey: string, ownerKey: string, charName: string): void {
26 + _prefs[userKey] = { ownerKey, charName };
27 + savePrefs();
28 + }
29 +
30 + export function clearPersistentPreference(userKey: string): void {
31 + delete _prefs[userKey];
32 + savePrefs();
33 + }
34 +
35 + export function getPersistentPreference(userKey: string): { ownerKey: string; charName: string } | null {
36 + return _prefs[userKey] ?? null;
37 + }
38 +
39 + // ─── Active borrow requests ───────────────────────────────────────────────────
40 + const pendingRequests: Map<string, BorrowRequest> = new Map();
41 + const sessionBorrows: Map<string, { ownerKey: string; charName: string }> = new Map();
42 + const borrowDmMessages: Map<string, { channelId: string; messageId: string }> = new Map();
43 +
44 + function requestKey(ownerKey: string, requesterKey: string): string {
45 + return `${ownerKey}:${requesterKey}`;
46 + }
47 +
48 + export function getPendingRequest(ownerKey: string, requesterKey: string): BorrowRequest | null {
49 + return pendingRequests.get(requestKey(ownerKey, requesterKey)) ?? null;
50 + }
51 +
52 + export function getPendingRequestByKey(key: string): BorrowRequest | null {
53 + return pendingRequests.get(key) ?? null;
54 + }
55 +
56 + export function getAllPendingForOwner(ownerKey: string): BorrowRequest[] {
57 + return [...pendingRequests.values()].filter((r) => r.ownerKey === ownerKey);
58 + }
59 +
60 + export function addPendingRequest(request: BorrowRequest): void {
61 + const key = requestKey(request.ownerKey, request.requesterKey);
62 + const expiry = cfg("borrowRequestExpiryMs" as any) ?? 0;
63 + pendingRequests.set(key, request);
64 + if (expiry > 0) {
65 + setTimeout(() => {
66 + if (pendingRequests.get(key)?.requestedAt === request.requestedAt) {
67 + pendingRequests.delete(key);
68 + console.log(`[borrow] Request ${key} expired.`);
69 + }
70 + }, expiry);
71 + }
72 + }
73 +
74 + export function removePendingRequest(ownerKey: string, requesterKey: string): void {
75 + pendingRequests.delete(requestKey(ownerKey, requesterKey));
76 + }
77 +
78 + export function storeDmMessage(ownerKey: string, requesterKey: string, channelId: string, messageId: string): void {
79 + borrowDmMessages.set(requestKey(ownerKey, requesterKey), { channelId, messageId });
80 + }
81 +
82 + export function getDmMessage(ownerKey: string, requesterKey: string): { channelId: string; messageId: string } | null {
83 + return borrowDmMessages.get(requestKey(ownerKey, requesterKey)) ?? null;
84 + }
85 +
86 + // ─── Session borrows ──────────────────────────────────────────────────────────
87 + export function setSessionBorrow(requesterKey: string, ownerKey: string, charName: string): void {
88 + sessionBorrows.set(requesterKey, { ownerKey, charName });
89 + }
90 +
91 + export function getSessionBorrow(requesterKey: string): { ownerKey: string; charName: string } | null {
92 + return sessionBorrows.get(requesterKey) ?? null;
93 + }
94 +
95 + export function clearSessionBorrows(): void {
96 + sessionBorrows.clear();
97 + borrowDmMessages.clear();
98 + }
99 +
100 + export function canUseCharacter(requesterKey: string, ownerKey: string, charName: string): boolean {
101 + if (requesterKey === ownerKey) return true;
102 + const char = getCharacterByName(ownerKey, charName);
103 + if (char?.sharedWith?.includes(requesterKey)) return true;
104 + const borrow = getSessionBorrow(requesterKey);
105 + if (borrow && borrow.ownerKey === ownerKey && borrow.charName.toLowerCase() === charName.toLowerCase()) return true;
106 + return false;
107 + }
108 +
109 + export function clearSessionBorrowForUser(userKey: string): void {
110 + sessionBorrows.delete(userKey);
111 + }
112 +
113 + // ─── DM notifications ─────────────────────────────────────────────────────────
114 + export async function sendBorrowRequestDM(
115 + client: Client,
116 + ownerDiscordId: string,
117 + requesterDisplayName: string,
118 + ownerKey: string,
119 + requesterKey: string,
120 + charName: string,
121 + charClass: string,
122 + charLevel: number,
123 + fallbackChannel?: TextChannel
124 + ): Promise<void> {
125 + const content = `🔔 **Borrow Request**\n**${requesterDisplayName}** wants to borrow **${charName}** (${charClass} · Lv${charLevel}) for tonight's TG.`;
126 +
127 + const acceptBtn = new ButtonBuilder()
128 + .setCustomId(`borrow_accept:${ownerKey}:${requesterKey}`)
129 + .setLabel("✅ Accept")
130 + .setStyle(ButtonStyle.Success);
131 +
132 + const declineBtn = new ButtonBuilder()
133 + .setCustomId(`borrow_decline:${ownerKey}:${requesterKey}`)
134 + .setLabel("❌ Decline")
135 + .setStyle(ButtonStyle.Danger);
136 +
137 + const row = new ActionRowBuilder<ButtonBuilder>().addComponents(acceptBtn, declineBtn);
138 +
139 + try {
140 + const ownerUser = await client.users.fetch(ownerDiscordId);
141 + const dm = await ownerUser.createDM();
142 + const msg = await dm.send({ content, components: [row] });
143 + storeDmMessage(ownerKey, requesterKey, dm.id, msg.id);
144 + } catch {
145 + if (fallbackChannel) {
146 + await fallbackChannel.send({
147 + content: `<@${ownerDiscordId}> ${content}\nUse \`/tg char accept ${requesterKey}\` or \`/tg char decline ${requesterKey}\`.`,
148 + });
149 + }
150 + }
151 + }
152 +
153 + export async function updateBorrowDM(
154 + client: Client,
155 + ownerKey: string,
156 + requesterKey: string,
157 + accepted: boolean
158 + ): Promise<void> {
159 + const dm = getDmMessage(ownerKey, requesterKey);
160 + if (!dm) return;
161 + try {
162 + const channel = await client.channels.fetch(dm.channelId) as any;
163 + const message = await channel.messages.fetch(dm.messageId);
164 + const status = accepted ? "✅ Accepted" : "❌ Declined";
165 + await message.edit({ content: `${message.content}\n\n*${status}*`, components: [] });
166 + } catch {}
167 + }
168 +
169 + // ─── Effective character resolution ──────────────────────────────────────────
170 + export function getEffectiveCharacter(userKey: string): { char: any; borrowedFrom: string | null } {
171 + const { getActiveCharacter, getCharacterByName: getChar } = require("./characters");
172 +
173 + // 1. Session borrow (temporary, resets on poll start)
174 + const borrow = getSessionBorrow(userKey);
175 + if (borrow) {
176 + const char = getChar(borrow.ownerKey, borrow.charName);
177 + if (char) return { char, borrowedFrom: borrow.ownerKey };
178 + }
179 +
180 + // 2. Persistent preference (survives restarts and poll resets)
181 + const pref = getPersistentPreference(userKey);
182 + console.log(`[getEffectiveCharacter] userKey=${userKey} sessionBorrow=${JSON.stringify(borrow)} pref=${JSON.stringify(pref)}`);
183 + if (pref) {
184 + const char = getChar(pref.ownerKey, pref.charName);
185 + if (char) return { char, borrowedFrom: pref.ownerKey };
186 + clearPersistentPreference(userKey);
187 + }
188 +
189 + // 3. Own active character
190 + const char = getActiveCharacter(userKey);
191 + return { char: char ?? null, borrowedFrom: null };
192 + }
Newer Older