Zuletzt aktiv 1 month ago

nuno hat die Gist bearbeitet 1 month ago. Zu Änderung gehen

1 file changed, 174 insertions

borrow.ts(Datei erstellt)

@@ -0,0 +1,174 @@
1 + import { Client, TextChannel } from "discord.js";
2 + import { BorrowRequest } from "../types";
3 + import { cfg } from "./config";
4 + import { getCharacterByName } from "./characters";
5 +
6 + // Active borrow requests (pending accept/decline)
7 + const pendingRequests: Map<string, BorrowRequest> = new Map(); // key: `${ownerKey}:${requesterKey}`
8 +
9 + // Session borrows: usermapKey → { ownerKey, charName } — reset on poll start
10 + const sessionBorrows: Map<string, { ownerKey: string; charName: string }> = new Map();
11 +
12 + // DM message IDs for updating borrow request messages
13 + const borrowDmMessages: Map<string, { channelId: string; messageId: string }> = new Map();
14 +
15 + function requestKey(ownerKey: string, requesterKey: string): string {
16 + return `${ownerKey}:${requesterKey}`;
17 + }
18 +
19 + export function getPendingRequest(ownerKey: string, requesterKey: string): BorrowRequest | null {
20 + return pendingRequests.get(requestKey(ownerKey, requesterKey)) ?? null;
21 + }
22 +
23 + export function getPendingRequestByKey(key: string): BorrowRequest | null {
24 + return pendingRequests.get(key) ?? null;
25 + }
26 +
27 + export function getAllPendingForOwner(ownerKey: string): BorrowRequest[] {
28 + return [...pendingRequests.values()].filter((r) => r.ownerKey === ownerKey);
29 + }
30 +
31 + export function addPendingRequest(request: BorrowRequest): void {
32 + const key = requestKey(request.ownerKey, request.requesterKey);
33 + const expiry = cfg("borrowRequestExpiryMs" as any) ?? 0;
34 +
35 + pendingRequests.set(key, request);
36 +
37 + if (expiry > 0) {
38 + setTimeout(() => {
39 + if (pendingRequests.get(key)?.requestedAt === request.requestedAt) {
40 + pendingRequests.delete(key);
41 + console.log(`[borrow] Request ${key} expired.`);
42 + }
43 + }, expiry);
44 + }
45 + }
46 +
47 + export function removePendingRequest(ownerKey: string, requesterKey: string): void {
48 + pendingRequests.delete(requestKey(ownerKey, requesterKey));
49 + }
50 +
51 + export function storeDmMessage(ownerKey: string, requesterKey: string, channelId: string, messageId: string): void {
52 + borrowDmMessages.set(requestKey(ownerKey, requesterKey), { channelId, messageId });
53 + }
54 +
55 + export function getDmMessage(ownerKey: string, requesterKey: string): { channelId: string; messageId: string } | null {
56 + return borrowDmMessages.get(requestKey(ownerKey, requesterKey)) ?? null;
57 + }
58 +
59 + // Session borrow management
60 + export function setSessionBorrow(requesterKey: string, ownerKey: string, charName: string): void {
61 + sessionBorrows.set(requesterKey, { ownerKey, charName });
62 + }
63 +
64 + export function getSessionBorrow(requesterKey: string): { ownerKey: string; charName: string } | null {
65 + return sessionBorrows.get(requesterKey) ?? null;
66 + }
67 +
68 + export function clearSessionBorrows(): void {
69 + sessionBorrows.clear();
70 + borrowDmMessages.clear();
71 + }
72 +
73 + // Check if a user can use a character (owns it or has share/borrow access)
74 + export function canUseCharacter(requesterKey: string, ownerKey: string, charName: string): boolean {
75 + if (requesterKey === ownerKey) return true;
76 +
77 + // Check persistent share
78 + const char = getCharacterByName(ownerKey, charName);
79 + if (char?.sharedWith?.includes(requesterKey)) return true;
80 +
81 + // Check session borrow
82 + const borrow = getSessionBorrow(requesterKey);
83 + if (borrow && borrow.ownerKey === ownerKey && borrow.charName.toLowerCase() === charName.toLowerCase()) return true;
84 +
85 + return false;
86 + }
87 +
88 + // Send borrow request DM to owner, fall back to poll channel ephemeral
89 + export async function sendBorrowRequestDM(
90 + client: Client,
91 + ownerDiscordId: string,
92 + requesterDisplayName: string,
93 + ownerKey: string,
94 + requesterKey: string,
95 + charName: string,
96 + charClass: string,
97 + charLevel: number,
98 + fallbackChannel?: TextChannel
99 + ): Promise<void> {
100 + const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = await import("discord.js");
101 +
102 + const content = `🔔 **Borrow Request**\n**${requesterDisplayName}** wants to borrow **«${charName}»** (${charClass} · Lv${charLevel}) for tonight's TG.`;
103 +
104 + const acceptBtn = new ButtonBuilder()
105 + .setCustomId(`borrow_accept:${ownerKey}:${requesterKey}`)
106 + .setLabel("✅ Accept")
107 + .setStyle(ButtonStyle.Success);
108 +
109 + const declineBtn = new ButtonBuilder()
110 + .setCustomId(`borrow_decline:${ownerKey}:${requesterKey}`)
111 + .setLabel("❌ Decline")
112 + .setStyle(ButtonStyle.Danger);
113 +
114 + const row = new ActionRowBuilder<ButtonBuilder>().addComponents(acceptBtn, declineBtn);
115 +
116 + try {
117 + const ownerUser = await client.users.fetch(ownerDiscordId);
118 + const dm = await ownerUser.createDM();
119 + const msg = await dm.send({ content, components: [row] });
120 + storeDmMessage(ownerKey, requesterKey, dm.id, msg.id);
121 + } catch {
122 + // DM failed — fall back to poll channel ephemeral
123 + if (fallbackChannel) {
124 + await fallbackChannel.send({
125 + content: `<@${ownerDiscordId}> ${content}\nUse \`/tg char accept ${requesterKey}\` or \`/tg char decline ${requesterKey}\`.`,
126 + });
127 + }
128 + }
129 + }
130 +
131 + // Update DM after accept/decline to disable buttons
132 + export async function updateBorrowDM(
133 + client: Client,
134 + ownerKey: string,
135 + requesterKey: string,
136 + accepted: boolean
137 + ): Promise<void> {
138 + const dm = getDmMessage(ownerKey, requesterKey);
139 + if (!dm) return;
140 + try {
141 + const channel = await client.channels.fetch(dm.channelId) as any;
142 + const message = await channel.messages.fetch(dm.messageId);
143 + const status = accepted ? "✅ Accepted" : "❌ Declined";
144 + await message.edit({ content: `${message.content}\n\n*${status}*`, components: [] });
145 + } catch {
146 + // DM may have been deleted, ignore
147 + }
148 + }
149 +
150 + // Returns the effective active character for a user
151 + // Priority: session borrow → persistent preference → own active character
152 + export function getEffectiveCharacter(usermapKey: string): { char: any; borrowedFrom: string | null } {
153 + const { getActiveCharacter, getCharacterByName } = require("./characters");
154 +
155 + // 1. Session borrow (temporary, resets on poll start)
156 + const borrow = getSessionBorrow(usermapKey);
157 + if (borrow) {
158 + const char = getCharacterByName(borrow.ownerKey, borrow.charName);
159 + if (char) return { char, borrowedFrom: borrow.ownerKey };
160 + }
161 +
162 + // 2. Persistent preference (set when switching to a shared char, survives restarts)
163 + const pref = getPersistentPreference(usermapKey);
164 + if (pref) {
165 + const char = getCharacterByName(pref.ownerKey, pref.charName);
166 + if (char) return { char, borrowedFrom: pref.ownerKey };
167 + // Char no longer exists or share was revoked — clear preference
168 + clearPersistentPreference(usermapKey);
169 + }
170 +
171 + // 3. Own active character
172 + const char = getActiveCharacter(usermapKey);
173 + return { char: char ?? null, borrowedFrom: null };
174 + }
Neuer Älter