423 lines
7.1 KiB
JavaScript
423 lines
7.1 KiB
JavaScript
const {
|
|
MessageFlags
|
|
} = require("discord.js");
|
|
|
|
const { mClient } = require("../index");
|
|
|
|
|
|
const DATABASE = "neo-db";
|
|
|
|
const COOKIE_COLLECTION = "items_cookies";
|
|
const COOLDOWN_COLLECTION = "cooldowns_cookies";
|
|
|
|
|
|
function getCollections() {
|
|
const db = mClient.db(DATABASE);
|
|
|
|
return {
|
|
db,
|
|
cookies: db.collection(COOKIE_COLLECTION),
|
|
cooldowns: db.collection(COOLDOWN_COLLECTION)
|
|
};
|
|
}
|
|
|
|
|
|
async function getCookieBalance(userId) {
|
|
const { cookies } = getCollections();
|
|
|
|
const result = await cookies.findOne(
|
|
{
|
|
userId
|
|
},
|
|
{
|
|
projection: {
|
|
_id: 0,
|
|
cookies: 1
|
|
}
|
|
}
|
|
);
|
|
|
|
return result?.cookies ?? 0;
|
|
}
|
|
|
|
|
|
async function changeCookies(
|
|
userId,
|
|
amount
|
|
) {
|
|
const { cookies } = getCollections();
|
|
|
|
return cookies.updateOne(
|
|
{
|
|
userId
|
|
},
|
|
{
|
|
$inc: {
|
|
cookies: amount
|
|
},
|
|
|
|
$setOnInsert: {
|
|
userId
|
|
}
|
|
},
|
|
{
|
|
upsert: true
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
async function transferCookies(
|
|
fromUserId,
|
|
toUserId,
|
|
amount
|
|
) {
|
|
if (
|
|
!Number.isInteger(amount) ||
|
|
amount <= 0
|
|
) {
|
|
throw new Error(
|
|
"INVALID_COOKIE_AMOUNT"
|
|
);
|
|
}
|
|
|
|
|
|
const { cookies } = getCollections();
|
|
|
|
|
|
const debit =
|
|
await cookies.updateOne(
|
|
{
|
|
userId: fromUserId,
|
|
|
|
cookies: {
|
|
$gte: amount
|
|
}
|
|
},
|
|
{
|
|
$inc: {
|
|
cookies: -amount
|
|
}
|
|
}
|
|
);
|
|
|
|
|
|
if (
|
|
debit.modifiedCount !== 1
|
|
) {
|
|
throw new Error(
|
|
"INSUFFICIENT_COOKIES"
|
|
);
|
|
}
|
|
|
|
|
|
try {
|
|
await cookies.updateOne(
|
|
{
|
|
userId: toUserId
|
|
},
|
|
{
|
|
$inc: {
|
|
cookies: amount
|
|
},
|
|
|
|
$setOnInsert: {
|
|
userId: toUserId
|
|
}
|
|
},
|
|
{
|
|
upsert: true
|
|
}
|
|
);
|
|
|
|
} catch (error) {
|
|
try {
|
|
await cookies.updateOne(
|
|
{
|
|
userId: fromUserId
|
|
},
|
|
{
|
|
$inc: {
|
|
cookies: amount
|
|
}
|
|
}
|
|
);
|
|
|
|
} catch (rollbackError) {
|
|
console.error(
|
|
"CRITICAL: Cookie rollback failed:",
|
|
rollbackError
|
|
);
|
|
}
|
|
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
|
|
async function getCooldown(
|
|
userId,
|
|
guildId,
|
|
type
|
|
) {
|
|
const { cooldowns } = getCollections();
|
|
|
|
const result =
|
|
await cooldowns.findOne(
|
|
{
|
|
userId,
|
|
guildId
|
|
},
|
|
{
|
|
projection: {
|
|
_id: 0,
|
|
[type]: 1
|
|
}
|
|
}
|
|
);
|
|
|
|
return result?.[type] ?? 0;
|
|
}
|
|
|
|
|
|
async function setCooldown(
|
|
userId,
|
|
guildId,
|
|
type,
|
|
duration
|
|
) {
|
|
const { cooldowns } = getCollections();
|
|
|
|
const now = getUnixTime();
|
|
const next = now + duration;
|
|
|
|
|
|
await cooldowns.updateOne(
|
|
{
|
|
userId,
|
|
guildId
|
|
},
|
|
{
|
|
$set: {
|
|
[type]: next
|
|
},
|
|
|
|
$setOnInsert: {
|
|
userId,
|
|
guildId
|
|
}
|
|
},
|
|
{
|
|
upsert: true
|
|
}
|
|
);
|
|
|
|
|
|
return next;
|
|
}
|
|
|
|
|
|
/*
|
|
* Atomically claim a cooldown.
|
|
*
|
|
* Useful for button games where a double-click
|
|
* could otherwise resolve twice.
|
|
*/
|
|
|
|
async function claimCooldown(
|
|
userId,
|
|
guildId,
|
|
type,
|
|
duration
|
|
) {
|
|
const { cooldowns } = getCollections();
|
|
|
|
const now = getUnixTime();
|
|
const next = now + duration;
|
|
|
|
|
|
try {
|
|
const result =
|
|
await cooldowns.updateOne(
|
|
{
|
|
userId,
|
|
guildId,
|
|
|
|
$or: [
|
|
{
|
|
[type]: {
|
|
$lte: now
|
|
}
|
|
},
|
|
{
|
|
[type]: {
|
|
$exists: false
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
$set: {
|
|
[type]: next
|
|
},
|
|
|
|
$setOnInsert: {
|
|
userId,
|
|
guildId
|
|
}
|
|
},
|
|
{
|
|
upsert: true
|
|
}
|
|
);
|
|
|
|
|
|
return {
|
|
claimed:
|
|
result.modifiedCount === 1 ||
|
|
result.upsertedCount === 1,
|
|
|
|
next
|
|
};
|
|
|
|
} catch (error) {
|
|
/*
|
|
* A unique {userId, guildId} index may cause
|
|
* a duplicate-key error if an active cooldown
|
|
* already exists.
|
|
*/
|
|
if (error?.code === 11000) {
|
|
const active =
|
|
await getCooldown(
|
|
userId,
|
|
guildId,
|
|
type
|
|
);
|
|
|
|
return {
|
|
claimed: false,
|
|
next: active
|
|
};
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
|
|
async function temporaryEphemeralReply(
|
|
interaction,
|
|
options,
|
|
deleteAfter = 5000
|
|
) {
|
|
await interaction.reply({
|
|
...options,
|
|
flags: MessageFlags.Ephemeral
|
|
});
|
|
|
|
|
|
setTimeout(
|
|
async () => {
|
|
try {
|
|
await interaction.deleteReply();
|
|
} catch {}
|
|
},
|
|
deleteAfter
|
|
);
|
|
}
|
|
|
|
|
|
async function temporaryEphemeralFollowUp(
|
|
interaction,
|
|
options,
|
|
deleteAfter = 8000
|
|
) {
|
|
const message =
|
|
await interaction.followUp({
|
|
...options,
|
|
flags: MessageFlags.Ephemeral
|
|
});
|
|
|
|
|
|
setTimeout(
|
|
async () => {
|
|
try {
|
|
await message.delete();
|
|
} catch {}
|
|
},
|
|
deleteAfter
|
|
);
|
|
|
|
|
|
return message;
|
|
}
|
|
|
|
|
|
function getUnixTime() {
|
|
return Math.floor(
|
|
Date.now() / 1000
|
|
);
|
|
}
|
|
|
|
|
|
function randomInteger(
|
|
min,
|
|
max
|
|
) {
|
|
return Math.floor(
|
|
Math.random() *
|
|
(max - min + 1)
|
|
) + min;
|
|
}
|
|
|
|
|
|
function randomFloat(
|
|
min,
|
|
max
|
|
) {
|
|
return (
|
|
Math.random() *
|
|
(max - min)
|
|
) + min;
|
|
}
|
|
|
|
|
|
function randomItem(items) {
|
|
return items[
|
|
Math.floor(
|
|
Math.random() *
|
|
items.length
|
|
)
|
|
];
|
|
}
|
|
|
|
|
|
function formatPercentage(value) {
|
|
return `${Math.round(value * 100)}%`;
|
|
}
|
|
|
|
|
|
module.exports = {
|
|
DATABASE,
|
|
COOKIE_COLLECTION,
|
|
COOLDOWN_COLLECTION,
|
|
|
|
getCollections,
|
|
getCookieBalance,
|
|
changeCookies,
|
|
transferCookies,
|
|
|
|
getCooldown,
|
|
setCooldown,
|
|
claimCooldown,
|
|
|
|
temporaryEphemeralReply,
|
|
temporaryEphemeralFollowUp,
|
|
|
|
getUnixTime,
|
|
randomInteger,
|
|
randomFloat,
|
|
randomItem,
|
|
formatPercentage
|
|
}; |