Остання активність 3 weeks ago

gistfile1.txt Неформатований
1import { Client, TextChannel, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
2import fs from "fs";
3import path from "path";
4import { BorrowRequest } from "@src/types";
5import { cfg } from "@systems/config";
6import { getCharacterByName } from "@systems/characters";
7
8const PREFS_PATH = path.join(__dirname, "../../data/sessionPreferences.json");
9
10// ─── Persistent preferences ───────────────────────────────────────────────────
11let _prefs: Record<string, { ownerKey: string; charName: string }> = {};
12
13function loadPrefs(): void {
14 try { _prefs = JSON.parse(fs.readFileSync(PREFS_PATH, "utf8")); }
15 catch { _prefs = {}; }
16}
17
18function 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
23loadPrefs();
24
25export function setPersistentPreference(userKey: string, ownerKey: string, charName: string): void {
26 _prefs[userKey] = { ownerKey, charName };
27 savePrefs();
28}
29
30export function clearPersistentPreference(userKey: string): void {
31 delete _prefs[userKey];
32 savePrefs();
33}
34
35export function getPersistentPreference(userKey: string): { ownerKey: string; charName: string } | null {
36 return _prefs[userKey] ?? null;
37}
38
39// ─── Active borrow requests ───────────────────────────────────────────────────
40const pendingRequests: Map<string, BorrowRequest> = new Map();
41const sessionBorrows: Map<string, { ownerKey: string; charName: string }> = new Map();
42const borrowDmMessages: Map<string, { channelId: string; messageId: string }> = new Map();
43
44function requestKey(ownerKey: string, requesterKey: string): string {
45 return `${ownerKey}:${requesterKey}`;
46}
47
48export function getPendingRequest(ownerKey: string, requesterKey: string): BorrowRequest | null {
49 return pendingRequests.get(requestKey(ownerKey, requesterKey)) ?? null;
50}
51
52export function getPendingRequestByKey(key: string): BorrowRequest | null {
53 return pendingRequests.get(key) ?? null;
54}
55
56export function getAllPendingForOwner(ownerKey: string): BorrowRequest[] {
57 return [...pendingRequests.values()].filter((r) => r.ownerKey === ownerKey);
58}
59
60export 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
74export function removePendingRequest(ownerKey: string, requesterKey: string): void {
75 pendingRequests.delete(requestKey(ownerKey, requesterKey));
76}
77
78export function storeDmMessage(ownerKey: string, requesterKey: string, channelId: string, messageId: string): void {
79 borrowDmMessages.set(requestKey(ownerKey, requesterKey), { channelId, messageId });
80}
81
82export function getDmMessage(ownerKey: string, requesterKey: string): { channelId: string; messageId: string } | null {
83 return borrowDmMessages.get(requestKey(ownerKey, requesterKey)) ?? null;
84}
85
86// ─── Session borrows ──────────────────────────────────────────────────────────
87export function setSessionBorrow(requesterKey: string, ownerKey: string, charName: string): void {
88 sessionBorrows.set(requesterKey, { ownerKey, charName });
89}
90
91export function getSessionBorrow(requesterKey: string): { ownerKey: string; charName: string } | null {
92 return sessionBorrows.get(requesterKey) ?? null;
93}
94
95export function clearSessionBorrows(): void {
96 sessionBorrows.clear();
97 borrowDmMessages.clear();
98}
99
100export 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
109export function clearSessionBorrowForUser(userKey: string): void {
110 sessionBorrows.delete(userKey);
111}
112
113// ─── DM notifications ─────────────────────────────────────────────────────────
114export 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
153export 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 ──────────────────────────────────────────
170export 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}