added bankruptcy

This commit is contained in:
DeSqBlocki
2026-08-23 00:09:13 +02:00
parent 030b4b8883
commit 9c867b1adb
12 changed files with 985 additions and 676 deletions
+11
View File
@@ -0,0 +1,11 @@
const {
closeInteractionSession
} = require("../../utils/interactionSession");
module.exports = {
name: "sessionClose",
async execute(interaction) {
return closeInteractionSession(interaction);
}
};
+91
View File
@@ -0,0 +1,91 @@
const {
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
MessageFlags
} = require("discord.js");
const {
declareBankruptcy
} = require("../../utils/bankruptcy");
const {
requireInteractionSession
} = require("../../utils/interactionSession");
module.exports = {
name: "crumbSaucerBankruptcy",
async execute(interaction) {
const session = await requireInteractionSession(interaction);
if (!session) {
return;
}
const result = await declareBankruptcy(interaction);
if (!result.ok) {
if (result.reason === "COOLDOWN") {
return interaction.reply({
content:
`Bankruptcy assistance is unavailable until <t:${result.nextAvailableAt}:R>.`,
flags: MessageFlags.Ephemeral
});
}
if (result.reason === "BALANCE_NOT_ZERO") {
return interaction.reply({
content:
`Bankruptcy can only be declared at exactly **0 Cookies**. Your current balance is **${result.balance.toLocaleString()}**.`,
flags: MessageFlags.Ephemeral
});
}
return interaction.reply({
content: "Bankruptcy could not be declared.",
flags: MessageFlags.Ephemeral
});
}
const embed = new EmbedBuilder()
.setTitle("🍪 Bankruptcy Assistance")
.setDescription(
[
`${interaction.user} declared bankruptcy and received a starter kit.`,
"",
`**+${result.amount.toLocaleString()} Cookies**`,
"",
"Spend them wisely. Or at least memorably."
].join("\n")
)
.addFields({
name: "Next Eligibility",
value: `<t:${result.nextAvailableAt}:R>`
})
.setFooter({
text: `crumbSaucer:${interaction.user.id}`
});
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId("crumbSaucerBack")
.setLabel("Back to Crumb Saucer")
.setEmoji("◀️")
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId("sessionClose")
.setLabel("Close")
.setEmoji("✖️")
.setStyle(ButtonStyle.Secondary)
);
return interaction.update({
embeds: [embed],
components: [row]
});
}
};
+64
View File
@@ -0,0 +1,64 @@
const {
SlashCommandBuilder,
EmbedBuilder,
MessageFlags
} = require("discord.js");
const {
declareBankruptcy
} = require("../../utils/bankruptcy");
module.exports = {
data: new SlashCommandBuilder()
.setName("bankruptcy")
.setDescription(
"Declare bankruptcy at exactly 0 Cookies and receive a starter kit."
),
async execute(interaction) {
const result = await declareBankruptcy(interaction);
if (!result.ok) {
if (result.reason === "COOLDOWN") {
return interaction.reply({
content:
`Bankruptcy assistance is unavailable until <t:${result.nextAvailableAt}:R>.`,
flags: MessageFlags.Ephemeral
});
}
if (result.reason === "BALANCE_NOT_ZERO") {
return interaction.reply({
content:
`You may only declare bankruptcy at exactly **0 Cookies**. Your current balance is **${result.balance.toLocaleString()}**.`,
flags: MessageFlags.Ephemeral
});
}
return interaction.reply({
content: "Bankruptcy could not be declared.",
flags: MessageFlags.Ephemeral
});
}
const embed = new EmbedBuilder()
.setTitle("🍪 Bankruptcy Declared")
.setDescription(
[
`${interaction.user} has been issued a fresh starter kit.`,
"",
`**+${result.amount.toLocaleString()} Cookies**`,
"",
"The Cookie economy believes in second chances."
].join("\n")
)
.addFields({
name: "Next Eligibility",
value: `<t:${result.nextAvailableAt}:R>`
});
return interaction.reply({
embeds: [embed]
});
}
};
+34 -51
View File
@@ -1,63 +1,46 @@
const { const { SlashCommandBuilder } = require("discord.js");
SlashCommandBuilder
} = require(
"discord.js"
);
const { const {
buildProfileOverview, buildProfileOverview,
buildProfileButtons buildProfileButtons
} = require( } = require("../../utils/profileSystem");
"../../utils/profileSystem" const {
); createInteractionSession
} = require("../../utils/interactionSession");
module.exports = { module.exports = {
data: data: new SlashCommandBuilder()
new SlashCommandBuilder() .setName("profile")
.setName( .setDescription("View a player profile")
"profile" .addUserOption(option =>
) option
.setDescription( .setName("user")
"View a player profile" .setDescription("Player to view")
) ),
.addUserOption(
option =>
option
.setName(
"user"
)
.setDescription(
"Player to view"
)
),
async execute( async execute(interaction) {
interaction const user = interaction.options.getUser("user") ?? interaction.user;
) {
const user =
interaction.options.getUser(
"user"
) ??
interaction.user;
const sessionOwnerId = const embed = await buildProfileOverview(
interaction.user.id; interaction.guildId,
user,
interaction.user.id
);
const embed = await interaction.reply({
await buildProfileOverview(
interaction.guildId,
user,
sessionOwnerId
);
return interaction.reply({
embeds: [embed], embeds: [embed],
components: components: buildProfileButtons()
buildProfileButtons(
"overview",
0,
1
)
}); });
const message = await interaction.fetchReply();
await createInteractionSession(
interaction,
{
messageId: message.id,
ownerId: interaction.user.id,
kind: "profile",
targetId: user.id
}
);
} }
}; };
+179 -275
View File
@@ -1,275 +1,179 @@
const { const {
Events, Events,
MessageFlags MessageFlags
} = require("discord.js"); } = require("discord.js");
require("dotenv").config(); const {
guardSessionInteraction
} = require("../utils/interactionSession");
module.exports = {
name: Events.InteractionCreate, require("dotenv").config();
once: false,
module.exports = {
async execute(interaction) { name: Events.InteractionCreate,
if (interaction.isAutocomplete()) { once: false,
return autocompleteHandler(interaction);
} async execute(interaction) {
if (interaction.isAutocomplete()) {
if (interaction.isChatInputCommand()) { return autocompleteHandler(interaction);
return commandHandler(interaction); }
}
if (interaction.isChatInputCommand()) {
if (interaction.isButton()) { return commandHandler(interaction);
return buttonHandler(interaction); }
}
if (interaction.isButton()) {
if ( return buttonHandler(interaction);
interaction.isMentionableSelectMenu() || }
interaction.isUserSelectMenu()
) { if (
return selectMenuHandler(interaction); interaction.isMentionableSelectMenu() ||
} interaction.isUserSelectMenu()
) {
if (interaction.isModalSubmit()) { return selectMenuHandler(interaction);
return modalHandler(interaction); }
}
} if (interaction.isModalSubmit()) {
}; return modalHandler(interaction);
}
}
async function autocompleteHandler(interaction) { };
const command =
interaction.client.commands.get( async function autocompleteHandler(interaction) {
interaction.commandName const command = interaction.client.commands.get(
); interaction.commandName
);
if (!command) { if (!command) {
return console.error( return console.error(
`No command matching ${interaction.commandName} was found.` `No command matching ${interaction.commandName} was found.`
); );
} }
if (typeof command.autocomplete !== "function") {
if ( return;
typeof command.autocomplete !== }
"function"
) { try {
return; await command.autocomplete(interaction);
} } catch (error) {
console.error(
`Autocomplete error for /${interaction.commandName}:`,
try { error
await command.autocomplete( );
interaction
); try {
await interaction.respond([]);
} catch (error) { } catch {}
console.error( }
`Autocomplete error for /${interaction.commandName}:`, }
error
); async function commandHandler(interaction) {
const command = interaction.client.commands.get(
interaction.commandName
/* );
* Autocomplete cannot use normal interaction.reply().
* If something failed, send an empty set so Discord if (!command) {
* stops waiting. return console.error(
*/ `No command matching ${interaction.commandName} was found.`
try { );
await interaction.respond([]); }
} catch {
// Interaction may already be acknowledged or expired. try {
} await command.execute(interaction);
} } catch (error) {
} console.error(error);
await sendInteractionError(
interaction,
async function commandHandler(interaction) { "There was an error while executing this command!"
const command = );
interaction.client.commands.get( }
interaction.commandName }
);
async function buttonHandler(interaction) {
const allowed = await guardSessionInteraction(interaction);
if (!command) {
return console.error( if (!allowed) {
`No command matching ${interaction.commandName} was found.` return;
); }
}
const button = interaction.client.buttons.get(
interaction.customId
try { );
await command.execute(
interaction if (!button) {
); return console.error(
`No button registered matching ${interaction.customId}.`
} catch (error) { );
console.error(error); }
if ( try {
interaction.replied || await button.execute(interaction);
interaction.deferred } catch (error) {
) { console.error(error);
await interaction.followUp({ await sendInteractionError(
content: interaction,
"There was an error while executing this command!", "There was an error while executing this button!"
flags: );
MessageFlags.Ephemeral }
}); }
} else { async function selectMenuHandler(interaction) {
await interaction.reply({ const selectMenu = interaction.client.selectMenus.get(
content: interaction.customId
"There was an error while executing this command!", );
flags:
MessageFlags.Ephemeral if (!selectMenu) {
}); return console.error(
} `No Select Menu registered matching ${interaction.customId}.`
} );
} }
try {
async function buttonHandler(interaction) { await selectMenu.execute(interaction);
const button = } catch (error) {
interaction.client.buttons.get( console.error(error);
interaction.customId await sendInteractionError(
); interaction,
"There was an error while executing this select menu!"
);
if (!button) { }
return console.error( }
`No button registered matching ${interaction.customId}.`
); async function modalHandler(interaction) {
} const customId = interaction.customId.split(":")[0];
const modal = interaction.client.modals.get(customId);
try { if (!modal) {
await button.execute( return console.error(
interaction `No Modal registered matching ${interaction.customId}.`
); );
}
} catch (error) {
console.error(error); try {
await modal.execute(interaction);
if ( } catch (error) {
interaction.replied || console.error(error);
interaction.deferred await sendInteractionError(
) { interaction,
await interaction.followUp({ "There was an error while executing this modal!"
content: );
"There was an error while executing this button!", }
flags: }
MessageFlags.Ephemeral
}); async function sendInteractionError(interaction, content) {
const payload = {
} else { content,
await interaction.reply({ flags: MessageFlags.Ephemeral
content: };
"There was an error while executing this button!",
flags: try {
MessageFlags.Ephemeral if (interaction.replied || interaction.deferred) {
}); await interaction.followUp(payload);
} } else {
} await interaction.reply(payload);
} }
} catch (error) {
console.error("Failed to send interaction error response:", error);
async function selectMenuHandler(interaction) { }
const selectMenu = }
interaction.client.selectMenus.get(
interaction.customId
);
if (!selectMenu) {
return console.error(
`No Select Menu registered matching ${interaction.customId}.`
);
}
try {
await selectMenu.execute(
interaction
);
} catch (error) {
console.error(error);
if (
interaction.replied ||
interaction.deferred
) {
await interaction.followUp({
content:
"There was an error while executing this select menu!",
flags:
MessageFlags.Ephemeral
});
} else {
await interaction.reply({
content:
"There was an error while executing this select menu!",
flags:
MessageFlags.Ephemeral
});
}
}
}
async function modalHandler(interaction) {
const customId =
interaction.customId
.split(":")[0];
const modal =
interaction.client
.modals
.get(
customId
);
if (!modal) {
return console.error(
`No Modal registered matching ${interaction.customId}.`
);
}
try {
await modal.execute(
interaction
);
} catch (error) {
console.error(error);
if (
interaction.replied ||
interaction.deferred
) {
await interaction.followUp({
content:
"There was an error while executing this modal!",
flags:
MessageFlags.Ephemeral
});
} else {
await interaction.reply({
content:
"There was an error while executing this modal!",
flags:
MessageFlags.Ephemeral
});
}
}
}
+47 -121
View File
@@ -5,156 +5,82 @@ const {
Partials Partials
} = require("discord.js"); } = require("discord.js");
const { const { MongoClient } = require("mongodb");
MongoClient
} = require("mongodb");
const fs = require("node:fs"); const fs = require("node:fs");
require("dotenv").config(); require("dotenv").config();
const mClient = new MongoClient(
process.env.M_URI
);
/* exports.mClient = mClient;
* MONGODB
*/
const mClient = const client = new Client({
new MongoClient( intents: [
process.env.M_URI GatewayIntentBits.Guilds,
); GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildPresences,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.MessageContent
],
partials: [
Partials.Channel,
Partials.Message,
Partials.User,
Partials.GuildMember,
Partials.Reaction
]
});
exports.mClient = exports.client = client;
mClient;
client.commands = new Collection();
/* client.legacyCommands = new Collection();
* DISCORD CLIENT client.aliases = new Collection();
*/ client.buttons = new Collection();
client.selectMenus = new Collection();
const client = client.modals = new Collection();
new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildPresences,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.MessageContent
],
partials: [
Partials.Channel,
Partials.Message,
Partials.User,
Partials.GuildMember,
Partials.Reaction
]
});
exports.client =
client;
/*
* COLLECTIONS
*/
client.commands =
new Collection();
client.legacyCommands =
new Collection();
client.aliases =
new Collection();
client.buttons =
new Collection();
client.selectMenus =
new Collection();
client.modals =
new Collection();
/*
* START APPLICATION
*/
async function start() { async function start() {
try { try {
/*
* Connect Mongo first.
*/
await mClient.connect(); await mClient.connect();
console.log("MongoDB connected");
console.log(
"MongoDB connected"
);
/*
* Initialize database indexes before handlers are
* exposed to Discord interactions.
*
* Requiring these after mClient is exported avoids
* circular-import initialization problems because the
* shared utilities import mClient from index.js.
*/
const { const {
ensureCookieIndexes ensureCookieIndexes
} = require( } = require("./utils/cookieEconomy");
"./utils/cookieEconomy"
);
const { const {
ensureProfileSystem ensureProfileSystem
} = require( } = require("./utils/profileSystem");
"./utils/profileSystem" const {
); ensureInteractionSessionIndexes
} = require("./utils/interactionSession");
const {
ensureBankruptcyIndexes
} = require("./utils/bankruptcy");
await Promise.all([ await Promise.all([
ensureCookieIndexes(), ensureCookieIndexes(),
ensureProfileSystem() ensureProfileSystem(),
ensureInteractionSessionIndexes(),
ensureBankruptcyIndexes()
]); ]);
console.log( console.log("Database indexes initialized");
"Database indexes initialized"
);
const handlerFiles = fs.readdirSync("./handlers");
/* for (const handler of handlerFiles) {
* Load handlers. require(`./handlers/${handler}`)(client);
*/
const handlerFiles =
fs.readdirSync(
"./handlers"
);
for (
const handler
of handlerFiles
) {
require(
`./handlers/${handler}`
)(client);
} }
/*
* Login Discord.
*/
await client.login( await client.login(
process.env.D_TOKEN process.env.D_TOKEN
); );
} catch (error) { } catch (error) {
console.error( console.error(
"Failed to start ShaiBot:", "Failed to start ShaiBot:",
+188
View File
@@ -0,0 +1,188 @@
const { getCollections, getCookieBalance, getUnixTime } = require("./cookieEconomy");
const { trackProfileEvent } = require("./profileSystem");
const BANKRUPTCY_COLLECTION = "items_bankruptcy";
const BANKRUPTCY_GRANT = 1000;
const BANKRUPTCY_COOLDOWN = 1 * 60 * 60;
function getBankruptcyCollection() {
return getCollections().db.collection(BANKRUPTCY_COLLECTION);
}
async function ensureBankruptcyIndexes() {
const bankruptcy = getBankruptcyCollection();
await bankruptcy.createIndex(
{ userId: 1, guildId: 1 },
{ unique: true }
);
}
async function getBankruptcyStatus(userId, guildId) {
const balance = await getCookieBalance(userId);
const document = await getBankruptcyCollection().findOne({
userId,
guildId
});
const now = getUnixTime();
const nextAvailableAt = Number(document?.nextAvailableAt ?? 0);
return {
balance,
eligible: balance === 0 && nextAvailableAt <= now,
nextAvailableAt,
claims: Number(document?.claims ?? 0)
};
}
async function declareBankruptcy(interaction, userId = interaction.user.id) {
const guildId = interaction.guildId;
if (!guildId) {
return {
ok: false,
reason: "GUILD_ONLY"
};
}
const { cookies } = getCollections();
const bankruptcy = getBankruptcyCollection();
const now = getUnixTime();
const previous = await bankruptcy.findOne({
userId,
guildId
});
const nextAvailableAt = Number(previous?.nextAvailableAt ?? 0);
if (nextAvailableAt > now) {
return {
ok: false,
reason: "COOLDOWN",
nextAvailableAt
};
}
let claimLock;
try {
claimLock = await bankruptcy.findOneAndUpdate(
{
userId,
guildId,
$or: [
{ nextAvailableAt: { $exists: false } },
{ nextAvailableAt: { $lte: now } }
]
},
{
$set: {
nextAvailableAt: now + BANKRUPTCY_COOLDOWN,
lastClaimedAt: now
},
$inc: {
claims: 1
},
$setOnInsert: {
userId,
guildId
}
},
{
upsert: true,
returnDocument: "after"
}
);
} catch (error) {
if (error?.code !== 11000) {
throw error;
}
}
if (!claimLock) {
const status = await getBankruptcyStatus(userId, guildId);
return {
ok: false,
reason: "COOLDOWN",
nextAvailableAt: status.nextAvailableAt
};
}
let credit;
try {
credit = await cookies.updateOne(
{
userId,
$or: [
{ cookies: 0 },
{ cookies: { $exists: false } }
]
},
{
$inc: {
cookies: BANKRUPTCY_GRANT
},
$setOnInsert: {
userId
}
},
{
upsert: true
}
);
} catch (error) {
if (error?.code !== 11000) {
throw error;
}
credit = { modifiedCount: 0, upsertedCount: 0 };
}
if (credit.modifiedCount !== 1 && credit.upsertedCount !== 1) {
await bankruptcy.updateOne(
{ userId, guildId },
{
$set: {
nextAvailableAt,
lastClaimedAt: previous?.lastClaimedAt ?? null,
claims: Number(previous?.claims ?? 0)
}
}
);
return {
ok: false,
reason: "BALANCE_NOT_ZERO",
balance: await getCookieBalance(userId)
};
}
await trackProfileEvent(
interaction,
userId,
{
type: "cookie_gain",
amount: BANKRUPTCY_GRANT
}
);
return {
ok: true,
amount: BANKRUPTCY_GRANT,
balance: BANKRUPTCY_GRANT,
nextAvailableAt: now + BANKRUPTCY_COOLDOWN,
claims: Number(claimLock.claims ?? 1)
};
}
module.exports = {
BANKRUPTCY_GRANT,
BANKRUPTCY_COOLDOWN,
ensureBankruptcyIndexes,
getBankruptcyStatus,
declareBankruptcy
};
+6 -6
View File
@@ -27,30 +27,30 @@ const HEIST_METHODS = {
baseChance: 0.65, baseChance: 0.65,
minStealPercent: 0.01, minStealPercent: 0.01,
maxStealPercent: 0.03, maxStealPercent: 0.03,
maxStealAmount: 1500, maxStealAmount: 5000,
minPenaltyPercent: 0.01, minPenaltyPercent: 0.01,
maxPenaltyPercent: 0.04, maxPenaltyPercent: 0.04,
maxPenaltyAmount: 750 maxPenaltyAmount: 1500
}, },
Hack: { Hack: {
baseChance: 0.45, baseChance: 0.45,
minStealPercent: 0.03, minStealPercent: 0.03,
maxStealPercent: 0.06, maxStealPercent: 0.06,
maxStealAmount: 3500, maxStealAmount: 12500,
minPenaltyPercent: 0.03, minPenaltyPercent: 0.03,
maxPenaltyPercent: 0.07, maxPenaltyPercent: 0.07,
maxPenaltyAmount: 2000 maxPenaltyAmount: 4000
}, },
Smash: { Smash: {
baseChance: 0.25, baseChance: 0.25,
minStealPercent: 0.06, minStealPercent: 0.06,
maxStealPercent: 0.12, maxStealPercent: 0.12,
maxStealAmount: 7500, maxStealAmount: 25000,
minPenaltyPercent: 0.06, minPenaltyPercent: 0.06,
maxPenaltyPercent: 0.12, maxPenaltyPercent: 0.12,
maxPenaltyAmount: 5000 maxPenaltyAmount: 10000
} }
}; };
+2 -1
View File
@@ -26,6 +26,7 @@ const {
const COLLECT_COOLDOWN = const COLLECT_COOLDOWN =
60 * 60; 60 * 60;
const COLLECT_MAX = 1000;
/* /*
* IMPORTANT: * IMPORTANT:
@@ -223,7 +224,7 @@ async function collectCookiesFromSaucer(
const amount = const amount =
Math.floor( Math.floor(
Math.random() * Math.random() *
1000 COLLECT_MAX
) )
+ +
1; 1;
+78 -219
View File
@@ -6,101 +6,56 @@ const {
} = require("discord.js"); } = require("discord.js");
const { const {
getCookieBalance, getCookieBalance
temporaryEphemeralReply
} = require("./cookieEconomy"); } = require("./cookieEconomy");
const {
createInteractionSession,
requireInteractionSession
} = require("./interactionSession");
/* async function showCrumbSaucer(interaction) {
* ============================================================ if (interaction.isButton()) {
* MAIN MENU const session = await requireInteractionSession(interaction);
* ============================================================
*/
async function showCrumbSaucer( if (!session) {
interaction return;
) {
/*
* Button navigation must preserve ownership.
*/
if (
interaction.isButton()
) {
const ownerId =
getCrumbSaucerSessionOwner(
interaction
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"This isn't your Crumb Saucer session."
}
);
} }
}
const balance = await getCookieBalance(interaction.user.id);
const embed = buildMainMenuEmbed(interaction.user, balance);
const balance =
await getCookieBalance(
interaction.user.id
);
const embed =
buildMainMenuEmbed(
interaction.user,
balance
);
if (
interaction.isButton()
) {
return interaction.update({ return interaction.update({
embeds: [ embeds: [embed],
embed components: buildMainMenuButtons()
],
components:
buildMainMenuButtons()
}); });
} }
const balance = await getCookieBalance(interaction.user.id);
const embed = buildMainMenuEmbed(interaction.user, balance);
return interaction.reply({ await interaction.reply({
embeds: [ embeds: [embed],
embed components: buildMainMenuButtons()
],
components:
buildMainMenuButtons()
}); });
const message = await interaction.fetchReply();
await createInteractionSession(
interaction,
{
messageId: message.id,
ownerId: interaction.user.id,
kind: "crumbSaucer"
}
);
return message;
} }
function buildMainMenuEmbed(user, balance) {
/*
* ============================================================
* MAIN MENU EMBED
* ============================================================
*/
function buildMainMenuEmbed(
user,
balance
) {
return new EmbedBuilder() return new EmbedBuilder()
.setTitle( .setTitle("🎰 The Crumb Saucer")
"🎰 The Crumb Saucer"
)
.setDescription( .setDescription(
[ [
`Welcome, ${user}!`, `Welcome, ${user}!`,
@@ -121,158 +76,78 @@ function buildMainMenuEmbed(
"Bet on Red, Black, Odd, Even, or Zero.", "Bet on Red, Black, Odd, Even, or Zero.",
"", "",
"### 🏦 Vault Heist", "### 🏦 Vault Heist",
"Attempt to breach ShaiBot's Cookie Vault." "Attempt to breach ShaiBot's Cookie Vault.",
"",
"### 🛟 Bankruptcy",
"At exactly 0 Cookies, claim a 1,000 Cookie starter kit."
].join("\n") ].join("\n")
) )
.addFields({ .addFields({
name: name: "Your Vault",
"Your Vault", value: `**${balance.toLocaleString()} 🍪**`
value:
`**${balance.toLocaleString()} 🍪**`
}) })
.setFooter({ .setFooter({
text: text: `crumbSaucer:${user.id}`
`crumbSaucer:${user.id}`
}); });
} }
/*
* ============================================================
* MAIN MENU BUTTONS
* ============================================================
*/
function buildMainMenuButtons() { function buildMainMenuButtons() {
return [ return [
new ActionRowBuilder() new ActionRowBuilder()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId( .setCustomId("crumbSaucerSlots")
"crumbSaucerSlots" .setLabel("Slots")
) .setEmoji("🎰")
.setLabel( .setStyle(ButtonStyle.Primary),
"Slots"
)
.setEmoji(
"🎰"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder() new ButtonBuilder()
.setCustomId( .setCustomId("crumbSaucerBlackjack")
"crumbSaucerBlackjack" .setLabel("Blackjack")
) .setEmoji("🃏")
.setLabel( .setStyle(ButtonStyle.Success),
"Blackjack"
)
.setEmoji(
"🃏"
)
.setStyle(
ButtonStyle.Success
),
new ButtonBuilder() new ButtonBuilder()
.setCustomId( .setCustomId("crumbSaucerRoulette")
"crumbSaucerRoulette" .setLabel("Roulette")
) .setEmoji("🎡")
.setLabel( .setStyle(ButtonStyle.Primary),
"Roulette"
)
.setEmoji(
"🎡"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder() new ButtonBuilder()
.setCustomId( .setCustomId("crumbSaucerHeist")
"crumbSaucerHeist" .setLabel("Vault Heist")
) .setEmoji("🏦")
.setLabel( .setStyle(ButtonStyle.Danger)
"Vault Heist"
)
.setEmoji(
"🏦"
)
.setStyle(
ButtonStyle.Danger
)
), ),
new ActionRowBuilder() new ActionRowBuilder()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId( .setCustomId("crumbSaucerCollectMenu")
"crumbSaucerCollectMenu" .setLabel("Cookie Bakery")
) .setEmoji("🍪")
.setLabel( .setStyle(ButtonStyle.Success),
"Cookie Bakery"
)
.setEmoji(
"🍪"
)
.setStyle(
ButtonStyle.Success
),
/*
* Do NOT use global "abort" here.
*
* The dedicated handler validates
* Crumb Saucer ownership first.
*/
new ButtonBuilder() new ButtonBuilder()
.setCustomId( .setCustomId("crumbSaucerBankruptcy")
"crumbSaucerClose" .setLabel("Bankruptcy")
) .setEmoji("🛟")
.setLabel( .setStyle(ButtonStyle.Secondary),
"Close"
) new ButtonBuilder()
.setEmoji( .setCustomId("sessionClose")
"✖️" .setLabel("Close")
) .setEmoji("✖️")
.setStyle( .setStyle(ButtonStyle.Secondary)
ButtonStyle.Secondary
)
) )
]; ];
} }
function getCrumbSaucerSessionOwner(interaction) {
const footer = interaction.message?.embeds?.[0]?.footer?.text;
const parts = footer?.split(":") ?? [];
/* switch (parts[0]) {
* ============================================================
* SESSION OWNER
* ============================================================
*/
function getCrumbSaucerSessionOwner(
interaction
) {
const footer =
interaction.message
?.embeds?.[0]
?.footer
?.text;
const parts =
footer?.split(":")
??
[];
switch (
parts[0]
) {
case "crumbSaucer": case "crumbSaucer":
case "crumbSaucerCollect": case "crumbSaucerCollect":
case "crumbSaucerSlots": case "crumbSaucerSlots":
@@ -283,35 +158,19 @@ function getCrumbSaucerSessionOwner(
case "crumbRouletteBet": case "crumbRouletteBet":
case "crumbRouletteResult": case "crumbRouletteResult":
case "cookieHeist": case "cookieHeist":
return parts[1]; return parts[1];
/*
* Active Blackjack:
*
* crumbBlackjack:<gameId>:<userId>
*/
case "crumbBlackjack": case "crumbBlackjack":
return parts[2]; return parts[2];
default: default:
return null; return null;
} }
} }
/*
* ============================================================
* EXPORTS
* ============================================================
*/
module.exports = { module.exports = {
showCrumbSaucer, showCrumbSaucer,
buildMainMenuEmbed,
buildMainMenuButtons,
getCrumbSaucerSessionOwner getCrumbSaucerSessionOwner
}; };
+284
View File
@@ -0,0 +1,284 @@
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
};
+1 -3
View File
@@ -1610,9 +1610,7 @@ function buildProfileButtons() {
), ),
new ButtonBuilder() new ButtonBuilder()
.setCustomId( .setCustomId("sessionClose")
"profileClose"
)
.setLabel( .setLabel(
"Close" "Close"
) )