const { EmbedBuilder, MessageFlags } = require("discord.js"); const { mClient } = require("../index"); const DATABASE_NAME = process.env.M_DB; const SESSION_COLLECTION = "items_sessions"; const DEFAULT_SESSION_LIFETIME_MS = 24 * 60 * 60 * 1000; function getSessionCollection() { if (!DATABASE_NAME) { throw new Error("M_DB is not configured in .env"); } return mClient.db(DATABASE_NAME).collection(SESSION_COLLECTION); } async function ensureInteractionSessionIndexes() { const sessions = getSessionCollection(); await sessions.createIndex( { messageId: 1 }, { unique: true } ); await sessions.createIndex( { expiresAt: 1 }, { expireAfterSeconds: 0 } ); } async function createInteractionSession( interaction, { messageId = null, ownerId = interaction.user.id, kind = "menu", targetId = null, metadata = {}, lifetimeMs = DEFAULT_SESSION_LIFETIME_MS } = {} ) { const resolvedMessageId = messageId ?? interaction.message?.id; if (!resolvedMessageId) { throw new Error("SESSION_MESSAGE_ID_REQUIRED"); } const sessions = getSessionCollection(); const now = new Date(); const expiresAt = new Date(now.getTime() + lifetimeMs); await sessions.updateOne( { messageId: resolvedMessageId }, { $set: { messageId: resolvedMessageId, ownerId, guildId: interaction.guildId ?? null, channelId: interaction.channelId ?? null, kind, targetId, metadata, active: true, updatedAt: now, expiresAt }, $setOnInsert: { createdAt: now } }, { upsert: true } ); return sessions.findOne({ messageId: resolvedMessageId }); } async function registerReplySession(interaction, options = {}) { const message = await interaction.fetchReply(); return createInteractionSession( interaction, { ...options, messageId: message.id } ); } async function getInteractionSession(interaction) { const messageId = interaction.message?.id; if (!messageId) { return null; } return getSessionCollection().findOne({ messageId, active: true, expiresAt: { $gt: new Date() } }); } function inferLegacyOwnerId(interaction) { const footer = interaction.message?.embeds?.[0]?.footer?.text; const parts = footer?.split(":") ?? []; if (!parts.length) { return null; } switch (parts[0]) { case "profile": case "crumbSaucer": case "crumbSaucerCollect": case "crumbSaucerSlots": case "crumbSaucerSlotsResult": case "crumbSaucerBlackjack": case "crumbBlackjackResult": case "crumbRouletteMenu": case "crumbRouletteBet": case "crumbRouletteResult": case "cookieHeist": return parts[1] ?? null; case "crumbBlackjack": return parts[2] ?? null; default: return null; } } async function replySessionError( interaction, content = "This isn't your session." ) { const payload = { content, flags: MessageFlags.Ephemeral }; if (interaction.replied || interaction.deferred) { return interaction.followUp(payload); } return interaction.reply(payload); } async function requireInteractionSession( interaction, { kinds = null, allowLegacy = true } = {} ) { const session = await getInteractionSession(interaction); if (session) { if (session.ownerId !== interaction.user.id) { await replySessionError(interaction); return null; } if ( Array.isArray(kinds) && kinds.length > 0 && !kinds.includes(session.kind) ) { await replySessionError( interaction, "This control belongs to a different session." ); return null; } return session; } if (allowLegacy) { const ownerId = inferLegacyOwnerId(interaction); if (ownerId) { if (ownerId !== interaction.user.id) { await replySessionError(interaction); return null; } return { messageId: interaction.message?.id ?? null, ownerId, guildId: interaction.guildId ?? null, channelId: interaction.channelId ?? null, kind: "legacy", targetId: null, metadata: {}, active: true, legacy: true }; } } return { messageId: interaction.message?.id ?? null, ownerId: interaction.user.id, kind: "unmanaged", metadata: {}, active: true, unmanaged: true }; } async function guardSessionInteraction(interaction) { if (!interaction.isButton()) { return true; } const session = await getInteractionSession(interaction); if (session) { if (session.ownerId === interaction.user.id) { return true; } await replySessionError(interaction); return false; } const ownerId = inferLegacyOwnerId(interaction); if (!ownerId || ownerId === interaction.user.id) { return true; } await replySessionError(interaction); return false; } async function closeInteractionSession(interaction) { const session = await requireInteractionSession(interaction); if (!session) { return false; } if (session.messageId && !session.unmanaged) { await getSessionCollection().updateOne( { messageId: session.messageId }, { $set: { active: false, closedAt: new Date(), updatedAt: new Date() } } ); } const embed = new EmbedBuilder() .setTitle("Session Closed") .setDescription("Closing menu. . ."); await interaction.update({ embeds: [embed], components: [] }); setTimeout(() => { interaction.deleteReply().catch(() => {}); }, 1200); return true; } module.exports = { ensureInteractionSessionIndexes, createInteractionSession, registerReplySession, getInteractionSession, requireInteractionSession, guardSessionInteraction, closeInteractionSession, replySessionError, inferLegacyOwnerId };