Files
ShaiBot/utils/interactionSession.js
T
2026-08-23 00:09:13 +02:00

285 lines
6.7 KiB
JavaScript

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
};