Compare commits
2 Commits
338b32430d
...
c2ac7737c0
| Author | SHA1 | Date | |
|---|---|---|---|
| c2ac7737c0 | |||
| 6a519f3a2d |
@@ -1,2 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
|
backup/
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
const {
|
||||||
|
buildProfileOverview,
|
||||||
|
buildProfileButtons
|
||||||
|
} = require("../../utils/profileSystem");
|
||||||
|
|
||||||
|
const {
|
||||||
|
temporaryEphemeralReply
|
||||||
|
} = require("../../utils/cookieEconomy");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: "crumbSaucerProfile",
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
const footer =
|
||||||
|
interaction.message
|
||||||
|
?.embeds?.[0]
|
||||||
|
?.footer
|
||||||
|
?.text;
|
||||||
|
|
||||||
|
const [
|
||||||
|
type,
|
||||||
|
ownerId
|
||||||
|
] = footer?.split(":") ?? [];
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
type !== "crumbSaucer" ||
|
||||||
|
ownerId !== interaction.user.id
|
||||||
|
) {
|
||||||
|
return temporaryEphemeralReply(
|
||||||
|
interaction,
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"This isn't your Crumb Saucer session."
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const embed =
|
||||||
|
await buildProfileOverview(
|
||||||
|
interaction.guildId,
|
||||||
|
interaction.user,
|
||||||
|
interaction.user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return interaction.update({
|
||||||
|
embeds: [embed],
|
||||||
|
components:
|
||||||
|
buildProfileButtons(
|
||||||
|
"overview",
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
const {
|
const {
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
PermissionFlagsBits,
|
|
||||||
MessageFlags,
|
MessageFlags,
|
||||||
EmbedBuilder
|
EmbedBuilder
|
||||||
} = require("discord.js");
|
} = require("discord.js");
|
||||||
@@ -13,6 +12,13 @@ const {
|
|||||||
"../../utils/profileMaintenance"
|
"../../utils/profileMaintenance"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
requireAdminAccess
|
||||||
|
} = require(
|
||||||
|
"../../utils/permissions"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
data:
|
data:
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
@@ -22,9 +28,6 @@ module.exports = {
|
|||||||
.setDescription(
|
.setDescription(
|
||||||
"Administrative profile maintenance tools"
|
"Administrative profile maintenance tools"
|
||||||
)
|
)
|
||||||
.setDefaultMemberPermissions(
|
|
||||||
PermissionFlagsBits.Administrator
|
|
||||||
)
|
|
||||||
.addSubcommand(
|
.addSubcommand(
|
||||||
subcommand =>
|
subcommand =>
|
||||||
subcommand
|
subcommand
|
||||||
@@ -69,14 +72,32 @@ module.exports = {
|
|||||||
async execute(
|
async execute(
|
||||||
interaction
|
interaction
|
||||||
) {
|
) {
|
||||||
|
/*
|
||||||
|
* Allows:
|
||||||
|
*
|
||||||
|
* - Discord Administrators
|
||||||
|
* - BOT_AUTHOR_ID from .env
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (
|
||||||
|
!await requireAdminAccess(
|
||||||
|
interaction
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const subcommand =
|
const subcommand =
|
||||||
interaction.options.getSubcommand();
|
interaction.options.getSubcommand();
|
||||||
|
|
||||||
|
|
||||||
await interaction.deferReply({
|
await interaction.deferReply({
|
||||||
flags:
|
flags:
|
||||||
MessageFlags.Ephemeral
|
MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
subcommand ===
|
subcommand ===
|
||||||
"reconcile"
|
"reconcile"
|
||||||
@@ -86,21 +107,26 @@ module.exports = {
|
|||||||
"user"
|
"user"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const dryRun =
|
const dryRun =
|
||||||
interaction.options.getBoolean(
|
interaction.options.getBoolean(
|
||||||
"dry-run"
|
"dry-run"
|
||||||
) ?? false;
|
) ?? false;
|
||||||
|
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
const result =
|
const result =
|
||||||
await reconcileUserAchievements({
|
await reconcileUserAchievements({
|
||||||
guildId:
|
guildId:
|
||||||
interaction.guildId,
|
interaction.guildId,
|
||||||
|
|
||||||
userId:
|
userId:
|
||||||
user.id,
|
user.id,
|
||||||
|
|
||||||
dryRun
|
dryRun
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
embeds: [
|
embeds: [
|
||||||
new EmbedBuilder()
|
new EmbedBuilder()
|
||||||
@@ -114,24 +140,30 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
name:
|
name:
|
||||||
"New unlocks",
|
"New unlocks",
|
||||||
|
|
||||||
value:
|
value:
|
||||||
`**${result.unlocked}**`,
|
`**${result.unlocked}**`,
|
||||||
|
|
||||||
inline:
|
inline:
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name:
|
name:
|
||||||
"Definitions checked",
|
"Definitions checked",
|
||||||
|
|
||||||
value:
|
value:
|
||||||
`**${result.checked}**`,
|
`**${result.checked}**`,
|
||||||
|
|
||||||
inline:
|
inline:
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name:
|
name:
|
||||||
"Event-only skipped",
|
"Event-only skipped",
|
||||||
|
|
||||||
value:
|
value:
|
||||||
`**${result.skippedEventOnly}**`,
|
`**${result.skippedEventOnly}**`,
|
||||||
|
|
||||||
inline:
|
inline:
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -140,13 +172,16 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const summary =
|
const summary =
|
||||||
await reconcileAllAchievements({
|
await reconcileAllAchievements({
|
||||||
guildId:
|
guildId:
|
||||||
interaction.guildId,
|
interaction.guildId,
|
||||||
|
|
||||||
dryRun
|
dryRun
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
embeds: [
|
embeds: [
|
||||||
new EmbedBuilder()
|
new EmbedBuilder()
|
||||||
@@ -160,24 +195,30 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
name:
|
name:
|
||||||
"Users",
|
"Users",
|
||||||
|
|
||||||
value:
|
value:
|
||||||
`**${summary.users}**`,
|
`**${summary.users}**`,
|
||||||
|
|
||||||
inline:
|
inline:
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name:
|
name:
|
||||||
"New unlocks",
|
"New unlocks",
|
||||||
|
|
||||||
value:
|
value:
|
||||||
`**${summary.unlocked}**`,
|
`**${summary.unlocked}**`,
|
||||||
|
|
||||||
inline:
|
inline:
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name:
|
name:
|
||||||
"Event-only checks skipped",
|
"Event-only checks skipped",
|
||||||
|
|
||||||
value:
|
value:
|
||||||
`**${summary.skippedEventOnly}**`,
|
`**${summary.skippedEventOnly}**`,
|
||||||
|
|
||||||
inline:
|
inline:
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -186,15 +227,23 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const audit =
|
const audit =
|
||||||
await auditProfileData(
|
await auditProfileData(
|
||||||
interaction.guildId
|
interaction.guildId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const issuePreview =
|
const issuePreview =
|
||||||
audit.issues
|
audit.issues
|
||||||
.slice(0, 10)
|
.slice(
|
||||||
.join("\n");
|
0,
|
||||||
|
10
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
"\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
embeds: [
|
embeds: [
|
||||||
@@ -206,48 +255,60 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
name:
|
name:
|
||||||
"Stats documents",
|
"Stats documents",
|
||||||
|
|
||||||
value:
|
value:
|
||||||
`**${audit.statDocuments}**`,
|
`**${audit.statDocuments}**`,
|
||||||
|
|
||||||
inline:
|
inline:
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name:
|
name:
|
||||||
"Achievement documents",
|
"Achievement documents",
|
||||||
|
|
||||||
value:
|
value:
|
||||||
`**${audit.achievementDocuments}**`,
|
`**${audit.achievementDocuments}**`,
|
||||||
|
|
||||||
inline:
|
inline:
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name:
|
name:
|
||||||
"Active achievements",
|
"Active achievements",
|
||||||
|
|
||||||
value:
|
value:
|
||||||
`**${audit.activeAchievements}**`,
|
`**${audit.activeAchievements}**`,
|
||||||
|
|
||||||
inline:
|
inline:
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name:
|
name:
|
||||||
"Global badges",
|
"Global badges",
|
||||||
|
|
||||||
value:
|
value:
|
||||||
`**${audit.badgeDocuments}** (${audit.claimedBadges} claimed)`,
|
`**${audit.badgeDocuments}** (${audit.claimedBadges} claimed)`,
|
||||||
|
|
||||||
inline:
|
inline:
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name:
|
name:
|
||||||
"Orphan achievement docs",
|
"Orphan achievement docs",
|
||||||
|
|
||||||
value:
|
value:
|
||||||
`**${audit.orphanAchievementDocs}**`,
|
`**${audit.orphanAchievementDocs}**`,
|
||||||
|
|
||||||
inline:
|
inline:
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name:
|
name:
|
||||||
"Issues",
|
"Issues",
|
||||||
|
|
||||||
value:
|
value:
|
||||||
`**${audit.issueCount}**`,
|
`**${audit.issueCount}**`,
|
||||||
|
|
||||||
inline:
|
inline:
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,30 +24,30 @@ const HEIST_COOLDOWN =
|
|||||||
|
|
||||||
const HEIST_METHODS = {
|
const HEIST_METHODS = {
|
||||||
Sneak: {
|
Sneak: {
|
||||||
baseChance: 0.65,
|
baseChance: 0.60,
|
||||||
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: 750
|
||||||
},
|
},
|
||||||
|
|
||||||
Hack: {
|
Hack: {
|
||||||
baseChance: 0.45,
|
baseChance: 0.40,
|
||||||
minStealPercent: 0.03,
|
minStealPercent: 0.03,
|
||||||
maxStealPercent: 0.06,
|
maxStealPercent: 0.06,
|
||||||
maxStealAmount: 3500,
|
maxStealAmount: 10000,
|
||||||
minPenaltyPercent: 0.03,
|
minPenaltyPercent: 0.03,
|
||||||
maxPenaltyPercent: 0.07,
|
maxPenaltyPercent: 0.07,
|
||||||
maxPenaltyAmount: 2000
|
maxPenaltyAmount: 2000
|
||||||
},
|
},
|
||||||
|
|
||||||
Smash: {
|
Smash: {
|
||||||
baseChance: 0.25,
|
baseChance: 0.20,
|
||||||
minStealPercent: 0.06,
|
minStealPercent: 0.06,
|
||||||
maxStealPercent: 0.12,
|
maxStealPercent: 0.12,
|
||||||
maxStealAmount: 7500,
|
maxStealAmount: 15000,
|
||||||
minPenaltyPercent: 0.06,
|
minPenaltyPercent: 0.06,
|
||||||
maxPenaltyPercent: 0.12,
|
maxPenaltyPercent: 0.12,
|
||||||
maxPenaltyAmount: 5000
|
maxPenaltyAmount: 5000
|
||||||
|
|||||||
@@ -339,7 +339,10 @@ async function runRouletteBack(
|
|||||||
return showRouletteChoices(
|
return showRouletteChoices(
|
||||||
interaction,
|
interaction,
|
||||||
bet,
|
bet,
|
||||||
false
|
false,
|
||||||
|
"run_it_back",
|
||||||
|
previousAllIn,
|
||||||
|
bet
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ function buildMainMenuButtons() {
|
|||||||
.setCustomId("crumbSaucerBlackjack")
|
.setCustomId("crumbSaucerBlackjack")
|
||||||
.setLabel("Blackjack")
|
.setLabel("Blackjack")
|
||||||
.setEmoji("🃏")
|
.setEmoji("🃏")
|
||||||
.setStyle(ButtonStyle.Success),
|
.setStyle(ButtonStyle.Primary),
|
||||||
|
|
||||||
new ButtonBuilder()
|
new ButtonBuilder()
|
||||||
.setCustomId("crumbSaucerRoulette")
|
.setCustomId("crumbSaucerRoulette")
|
||||||
@@ -126,19 +126,22 @@ function buildMainMenuButtons() {
|
|||||||
.setCustomId("crumbSaucerCollectMenu")
|
.setCustomId("crumbSaucerCollectMenu")
|
||||||
.setLabel("Cookie Bakery")
|
.setLabel("Cookie Bakery")
|
||||||
.setEmoji("🍪")
|
.setEmoji("🍪")
|
||||||
.setStyle(ButtonStyle.Success),
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
|
||||||
new ButtonBuilder()
|
new ButtonBuilder()
|
||||||
.setCustomId("crumbSaucerBankruptcy")
|
.setCustomId("crumbSaucerBankruptcy")
|
||||||
.setLabel("Bankruptcy")
|
.setLabel("Bankruptcy")
|
||||||
.setEmoji("🛟")
|
.setEmoji("🛟")
|
||||||
.setStyle(ButtonStyle.Secondary),
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId("crumbSaucerProfile")
|
||||||
|
.setLabel("Profile")
|
||||||
|
.setEmoji("👤")
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
new ButtonBuilder()
|
new ButtonBuilder()
|
||||||
.setCustomId("sessionClose")
|
.setCustomId("sessionClose")
|
||||||
.setLabel("Close")
|
.setLabel("Close")
|
||||||
.setEmoji("✖️")
|
.setEmoji("✖️")
|
||||||
.setStyle(ButtonStyle.Secondary)
|
.setStyle(ButtonStyle.Danger)
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-148
@@ -1,118 +1,46 @@
|
|||||||
const {
|
const {
|
||||||
PermissionFlagsBits,
|
PermissionFlagsBits,
|
||||||
MessageFlags
|
MessageFlags
|
||||||
} = require(
|
} = require("discord.js");
|
||||||
"discord.js"
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ============================================================
|
|
||||||
* CONFIG
|
|
||||||
* ============================================================
|
|
||||||
*
|
|
||||||
* .env:
|
|
||||||
*
|
|
||||||
* BOT_AUTHOR_ID=123456789012345678
|
|
||||||
*
|
|
||||||
* Do not hard-code the author ID into commands.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function getBotAuthorId() {
|
function getBotAuthorId() {
|
||||||
return (
|
return process.env.BOT_AUTHOR_ID ?? null;
|
||||||
process.env.BOT_AUTHOR_ID
|
|
||||||
??
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ============================================================
|
|
||||||
* BOT AUTHOR
|
|
||||||
* ============================================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
function isBotAuthor(
|
function isBotAuthor(
|
||||||
userId
|
userId
|
||||||
) {
|
) {
|
||||||
const authorId =
|
const authorId =
|
||||||
getBotAuthorId();
|
getBotAuthorId();
|
||||||
|
|
||||||
|
return Boolean(
|
||||||
if (
|
authorId &&
|
||||||
!authorId
|
userId &&
|
||||||
||
|
String(userId) === String(authorId)
|
||||||
!userId
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
String(
|
|
||||||
userId
|
|
||||||
)
|
|
||||||
===
|
|
||||||
String(
|
|
||||||
authorId
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ============================================================
|
|
||||||
* GUILD ADMINISTRATOR
|
|
||||||
* ============================================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
function isAdministrator(
|
function isAdministrator(
|
||||||
interaction
|
interaction
|
||||||
) {
|
) {
|
||||||
if (
|
return (
|
||||||
!interaction
|
interaction
|
||||||
?.memberPermissions
|
?.memberPermissions
|
||||||
) {
|
?.has(
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return interaction
|
|
||||||
.memberPermissions
|
|
||||||
.has(
|
|
||||||
PermissionFlagsBits.Administrator
|
PermissionFlagsBits.Administrator
|
||||||
|
)
|
||||||
|
??
|
||||||
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ============================================================
|
|
||||||
* ADMIN COMMAND ACCESS
|
|
||||||
* ============================================================
|
|
||||||
*
|
|
||||||
* Access is granted when:
|
|
||||||
*
|
|
||||||
* - the user is the configured bot author
|
|
||||||
* OR
|
|
||||||
* - the user has Discord Administrator
|
|
||||||
*
|
|
||||||
* Bot author is checked first so the owner can use admin
|
|
||||||
* utilities even without Discord Administrator in the guild.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function canUseAdminCommands(
|
function canUseAdminCommands(
|
||||||
interaction
|
interaction
|
||||||
) {
|
) {
|
||||||
if (
|
|
||||||
!interaction
|
|
||||||
?.user
|
|
||||||
?.id
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isBotAuthor(
|
isBotAuthor(
|
||||||
interaction.user.id
|
interaction.user.id
|
||||||
@@ -125,57 +53,6 @@ function canUseAdminCommands(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ============================================================
|
|
||||||
* STANDARD ACCESS DENIAL
|
|
||||||
* ============================================================
|
|
||||||
*
|
|
||||||
* Administrative access failures are intentionally ephemeral.
|
|
||||||
*/
|
|
||||||
|
|
||||||
async function denyAdminAccess(
|
|
||||||
interaction,
|
|
||||||
message =
|
|
||||||
"You don't have permission to use this command."
|
|
||||||
) {
|
|
||||||
const payload = {
|
|
||||||
content:
|
|
||||||
message,
|
|
||||||
|
|
||||||
flags:
|
|
||||||
MessageFlags.Ephemeral
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
if (
|
|
||||||
interaction.replied
|
|
||||||
||
|
|
||||||
interaction.deferred
|
|
||||||
) {
|
|
||||||
return interaction.followUp(
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return interaction.reply(
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ============================================================
|
|
||||||
* REQUIRE ADMIN ACCESS
|
|
||||||
* ============================================================
|
|
||||||
*
|
|
||||||
* Convenience helper:
|
|
||||||
*
|
|
||||||
* if (!await requireAdminAccess(interaction)) {
|
|
||||||
* return;
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
|
|
||||||
async function requireAdminAccess(
|
async function requireAdminAccess(
|
||||||
interaction
|
interaction
|
||||||
) {
|
) {
|
||||||
@@ -188,29 +65,39 @@ async function requireAdminAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await denyAdminAccess(
|
const payload = {
|
||||||
interaction
|
content:
|
||||||
|
"You don't have permission to use this command.",
|
||||||
|
|
||||||
|
flags:
|
||||||
|
MessageFlags.Ephemeral
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
interaction.replied ||
|
||||||
|
interaction.deferred
|
||||||
|
) {
|
||||||
|
await interaction.followUp(
|
||||||
|
payload
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
await interaction.reply(
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ============================================================
|
|
||||||
* EXPORTS
|
|
||||||
* ============================================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getBotAuthorId,
|
getBotAuthorId,
|
||||||
|
|
||||||
isBotAuthor,
|
isBotAuthor,
|
||||||
isAdministrator,
|
isAdministrator,
|
||||||
|
|
||||||
canUseAdminCommands,
|
canUseAdminCommands,
|
||||||
requireAdminAccess,
|
requireAdminAccess
|
||||||
|
|
||||||
denyAdminAccess
|
|
||||||
};
|
};
|
||||||
+459
-8
@@ -18,6 +18,352 @@ const COLLECTIONS = {
|
|||||||
achievementConfig: "config_achievements"
|
achievementConfig: "config_achievements"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PROFILE_TIMEZONE =
|
||||||
|
process.env.PROFILE_TIMEZONE ??
|
||||||
|
"Europe/Berlin";
|
||||||
|
|
||||||
|
const WEEKDAYS = [
|
||||||
|
"monday",
|
||||||
|
"tuesday",
|
||||||
|
"wednesday",
|
||||||
|
"thursday",
|
||||||
|
"friday",
|
||||||
|
"saturday",
|
||||||
|
"sunday"
|
||||||
|
];
|
||||||
|
function getActivityDateContext(
|
||||||
|
date = new Date()
|
||||||
|
) {
|
||||||
|
const formatter =
|
||||||
|
new Intl.DateTimeFormat(
|
||||||
|
"en-CA",
|
||||||
|
{
|
||||||
|
timeZone:
|
||||||
|
PROFILE_TIMEZONE,
|
||||||
|
|
||||||
|
year:
|
||||||
|
"numeric",
|
||||||
|
|
||||||
|
month:
|
||||||
|
"2-digit",
|
||||||
|
|
||||||
|
day:
|
||||||
|
"2-digit",
|
||||||
|
|
||||||
|
weekday:
|
||||||
|
"long",
|
||||||
|
|
||||||
|
hour:
|
||||||
|
"2-digit",
|
||||||
|
|
||||||
|
hourCycle:
|
||||||
|
"h23"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const parts =
|
||||||
|
Object.fromEntries(
|
||||||
|
formatter
|
||||||
|
.formatToParts(date)
|
||||||
|
.filter(
|
||||||
|
part =>
|
||||||
|
part.type !==
|
||||||
|
"literal"
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
part => [
|
||||||
|
part.type,
|
||||||
|
part.value
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const dateKey =
|
||||||
|
`${parts.year}-${parts.month}-${parts.day}`;
|
||||||
|
|
||||||
|
const weekday =
|
||||||
|
String(
|
||||||
|
parts.weekday
|
||||||
|
).toLowerCase();
|
||||||
|
|
||||||
|
const hour =
|
||||||
|
Number(
|
||||||
|
parts.hour
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dateKey,
|
||||||
|
yesterdayKey:
|
||||||
|
getPreviousDateKey(
|
||||||
|
dateKey
|
||||||
|
),
|
||||||
|
|
||||||
|
weekday,
|
||||||
|
hour,
|
||||||
|
|
||||||
|
weekKey:
|
||||||
|
getIsoWeekKey(
|
||||||
|
dateKey
|
||||||
|
),
|
||||||
|
|
||||||
|
timeBucket:
|
||||||
|
getTimeBucket(
|
||||||
|
hour
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getPreviousDateKey(
|
||||||
|
dateKey
|
||||||
|
) {
|
||||||
|
const date =
|
||||||
|
new Date(
|
||||||
|
`${dateKey}T00:00:00Z`
|
||||||
|
);
|
||||||
|
|
||||||
|
date.setUTCDate(
|
||||||
|
date.getUTCDate() - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
return date
|
||||||
|
.toISOString()
|
||||||
|
.slice(
|
||||||
|
0,
|
||||||
|
10
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getIsoWeekKey(
|
||||||
|
dateKey
|
||||||
|
) {
|
||||||
|
const date =
|
||||||
|
new Date(
|
||||||
|
`${dateKey}T00:00:00Z`
|
||||||
|
);
|
||||||
|
|
||||||
|
const day =
|
||||||
|
date.getUTCDay() || 7;
|
||||||
|
|
||||||
|
date.setUTCDate(
|
||||||
|
date.getUTCDate() +
|
||||||
|
4 -
|
||||||
|
day
|
||||||
|
);
|
||||||
|
|
||||||
|
const year =
|
||||||
|
date.getUTCFullYear();
|
||||||
|
|
||||||
|
const yearStart =
|
||||||
|
new Date(
|
||||||
|
Date.UTC(
|
||||||
|
year,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const week =
|
||||||
|
Math.ceil(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
date -
|
||||||
|
yearStart
|
||||||
|
)
|
||||||
|
/
|
||||||
|
86400000
|
||||||
|
+
|
||||||
|
1
|
||||||
|
)
|
||||||
|
/
|
||||||
|
7
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
`${year}-W${String(week).padStart(2, "0")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getTimeBucket(
|
||||||
|
hour
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
hour >= 0 &&
|
||||||
|
hour <= 5
|
||||||
|
) {
|
||||||
|
return "overnight";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
hour >= 6 &&
|
||||||
|
hour <= 11
|
||||||
|
) {
|
||||||
|
return "morning";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
hour >= 12 &&
|
||||||
|
hour <= 17
|
||||||
|
) {
|
||||||
|
return "afternoon";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "evening";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function trackGameActivity(
|
||||||
|
userId,
|
||||||
|
guildId
|
||||||
|
) {
|
||||||
|
const { stats } =
|
||||||
|
getProfileCollections();
|
||||||
|
|
||||||
|
const {
|
||||||
|
dateKey,
|
||||||
|
yesterdayKey,
|
||||||
|
weekday,
|
||||||
|
weekKey,
|
||||||
|
timeBucket
|
||||||
|
} =
|
||||||
|
getActivityDateContext();
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Every completed game increments its weekday
|
||||||
|
* and time-bucket counters.
|
||||||
|
*/
|
||||||
|
|
||||||
|
await stats.updateOne(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
guildId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$inc: {
|
||||||
|
[`activity.gamesByWeekday.${weekday}`]:
|
||||||
|
1,
|
||||||
|
|
||||||
|
[`activity.gamesByTimeBucket.${timeBucket}`]:
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Calendar-day state is only updated once per
|
||||||
|
* player per local calendar day.
|
||||||
|
*
|
||||||
|
* The filter also protects against two games
|
||||||
|
* resolving at nearly the same moment.
|
||||||
|
*/
|
||||||
|
|
||||||
|
await stats.updateOne(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
guildId,
|
||||||
|
|
||||||
|
"activity.lastGameDate": {
|
||||||
|
$ne:
|
||||||
|
dateKey
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
"activity.currentDailyStreak": {
|
||||||
|
$cond: [
|
||||||
|
{
|
||||||
|
$eq: [
|
||||||
|
"$activity.lastGameDate",
|
||||||
|
yesterdayKey
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
$add: [
|
||||||
|
{
|
||||||
|
$ifNull: [
|
||||||
|
"$activity.currentDailyStreak",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"activity.currentWeekDays": {
|
||||||
|
$cond: [
|
||||||
|
{
|
||||||
|
$eq: [
|
||||||
|
"$activity.currentWeekKey",
|
||||||
|
weekKey
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
$setUnion: [
|
||||||
|
{
|
||||||
|
$ifNull: [
|
||||||
|
"$activity.currentWeekDays",
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
[
|
||||||
|
weekday
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
[
|
||||||
|
weekday
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"activity.currentWeekKey":
|
||||||
|
weekKey,
|
||||||
|
|
||||||
|
"activity.lastActiveDate":
|
||||||
|
dateKey,
|
||||||
|
|
||||||
|
"activity.lastGameDate":
|
||||||
|
dateKey
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
"activity.bestDailyStreak": {
|
||||||
|
$max: [
|
||||||
|
{
|
||||||
|
$ifNull: [
|
||||||
|
"$activity.bestDailyStreak",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
$ifNull: [
|
||||||
|
"$activity.currentDailyStreak",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getProfileCollections() {
|
function getProfileCollections() {
|
||||||
if (!DATABASE_NAME) {
|
if (!DATABASE_NAME) {
|
||||||
throw new Error("M_DB is not configured in .env");
|
throw new Error("M_DB is not configured in .env");
|
||||||
@@ -142,7 +488,37 @@ function getDefaultStats(userId, guildId) {
|
|||||||
|
|
||||||
special: {
|
special: {
|
||||||
allInRunItBack: 0
|
allInRunItBack: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
activity: {
|
||||||
|
currentDailyStreak: 0,
|
||||||
|
bestDailyStreak: 0,
|
||||||
|
|
||||||
|
lastActiveDate: null,
|
||||||
|
lastGameDate: null,
|
||||||
|
|
||||||
|
currentWeekKey: null,
|
||||||
|
currentWeekDays: [],
|
||||||
|
|
||||||
|
gamesByWeekday: {
|
||||||
|
monday: 0,
|
||||||
|
tuesday: 0,
|
||||||
|
wednesday: 0,
|
||||||
|
thursday: 0,
|
||||||
|
friday: 0,
|
||||||
|
saturday: 0,
|
||||||
|
sunday: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
gamesByTimeBucket: {
|
||||||
|
overnight: 0,
|
||||||
|
morning: 0,
|
||||||
|
afternoon: 0,
|
||||||
|
evening: 0
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
profileSchemaVersion: 2
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,11 +669,17 @@ async function trackProfileEvent(
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "game_result":
|
case "game_result":
|
||||||
|
await trackGameActivity(
|
||||||
|
userId,
|
||||||
|
guildId
|
||||||
|
);
|
||||||
|
|
||||||
await trackGameResult(
|
await trackGameResult(
|
||||||
userId,
|
userId,
|
||||||
guildId,
|
guildId,
|
||||||
event
|
event
|
||||||
);
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "game_start":
|
case "game_start":
|
||||||
@@ -738,6 +1120,7 @@ async function unlockAchievement(
|
|||||||
|
|
||||||
await sendAchievementNotification(
|
await sendAchievementNotification(
|
||||||
interaction,
|
interaction,
|
||||||
|
userId,
|
||||||
definition
|
definition
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -775,24 +1158,63 @@ function readStatValue(
|
|||||||
|
|
||||||
async function sendAchievementNotification(
|
async function sendAchievementNotification(
|
||||||
interaction,
|
interaction,
|
||||||
|
userId,
|
||||||
achievement
|
achievement
|
||||||
) {
|
) {
|
||||||
try {
|
const embed =
|
||||||
await interaction.followUp({
|
|
||||||
embeds: [
|
|
||||||
new EmbedBuilder()
|
new EmbedBuilder()
|
||||||
.setTitle("🏆 Achievement Unlocked!")
|
.setTitle(
|
||||||
|
"🏆 Achievement Unlocked!"
|
||||||
|
)
|
||||||
.setDescription(
|
.setDescription(
|
||||||
[
|
[
|
||||||
`<@${userId}> earned an achievement!`,
|
`<@${userId}> earned an achievement!`,
|
||||||
"",
|
"",
|
||||||
`**${achievement.name}**`,
|
`${achievement.emoji ?? "🏆"} **${achievement.name}**`,
|
||||||
achievement.description
|
"",
|
||||||
|
achievement.description ??
|
||||||
|
achievement.requirement ??
|
||||||
|
"Achievement unlocked."
|
||||||
].join("\n")
|
].join("\n")
|
||||||
)
|
)
|
||||||
]
|
.setTimestamp();
|
||||||
|
|
||||||
|
try {
|
||||||
|
/*
|
||||||
|
* If the interaction has already been acknowledged,
|
||||||
|
* followUp is safe.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
interaction.replied ||
|
||||||
|
interaction.deferred
|
||||||
|
) {
|
||||||
|
return await interaction.followUp({
|
||||||
|
embeds: [embed]
|
||||||
});
|
});
|
||||||
} catch { }
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Otherwise don't consume the interaction's primary
|
||||||
|
* reply. The game/button handler still needs it.
|
||||||
|
*
|
||||||
|
* Send the public achievement announcement directly
|
||||||
|
* to the channel instead.
|
||||||
|
*/
|
||||||
|
if (interaction.channel) {
|
||||||
|
return await interaction.channel.send({
|
||||||
|
embeds: [embed]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (error) {
|
||||||
|
console.error(
|
||||||
|
`[Achievement] Failed to announce ${achievement.achievementId} for ${userId}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function evaluateBadges(
|
async function evaluateBadges(
|
||||||
@@ -1351,6 +1773,15 @@ async function buildStatsEmbed(
|
|||||||
const streaks =
|
const streaks =
|
||||||
stats.streaks ?? {};
|
stats.streaks ?? {};
|
||||||
|
|
||||||
|
const activity =
|
||||||
|
stats.activity ?? {};
|
||||||
|
|
||||||
|
const gamesByWeekday =
|
||||||
|
activity.gamesByWeekday ?? {};
|
||||||
|
|
||||||
|
const gamesByTimeBucket =
|
||||||
|
activity.gamesByTimeBucket ?? {};
|
||||||
|
|
||||||
return new EmbedBuilder()
|
return new EmbedBuilder()
|
||||||
.setTitle(
|
.setTitle(
|
||||||
`📊 ${user.globalName ?? user.username}'s Stats`
|
`📊 ${user.globalName ?? user.username}'s Stats`
|
||||||
@@ -1390,6 +1821,23 @@ async function buildStatsEmbed(
|
|||||||
].join("\n"),
|
].join("\n"),
|
||||||
inline: true
|
inline: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "📅 Activity",
|
||||||
|
|
||||||
|
value: [
|
||||||
|
`Daily streak: **${activity.currentDailyStreak ?? 0} days**`,
|
||||||
|
`Best streak: **${activity.bestDailyStreak ?? 0} days**`,
|
||||||
|
"",
|
||||||
|
`Mon **${gamesByWeekday.monday ?? 0}** • Tue **${gamesByWeekday.tuesday ?? 0}** • Wed **${gamesByWeekday.wednesday ?? 0}**`,
|
||||||
|
`Thu **${gamesByWeekday.thursday ?? 0}** • Fri **${gamesByWeekday.friday ?? 0}**`,
|
||||||
|
`Sat **${gamesByWeekday.saturday ?? 0}** • Sun **${gamesByWeekday.sunday ?? 0}**`,
|
||||||
|
"",
|
||||||
|
`🌙 Overnight **${gamesByTimeBucket.overnight ?? 0}**`,
|
||||||
|
`🌅 Morning **${gamesByTimeBucket.morning ?? 0}**`,
|
||||||
|
`☀️ Afternoon **${gamesByTimeBucket.afternoon ?? 0}**`,
|
||||||
|
`🌆 Evening **${gamesByTimeBucket.evening ?? 0}**`
|
||||||
|
].join("\n")
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "🎮 Per Game",
|
name: "🎮 Per Game",
|
||||||
value:
|
value:
|
||||||
@@ -2198,6 +2646,9 @@ module.exports = {
|
|||||||
getDefaultStats,
|
getDefaultStats,
|
||||||
readStatValue,
|
readStatValue,
|
||||||
|
|
||||||
|
getActivityDateContext,
|
||||||
|
trackGameActivity,
|
||||||
|
|
||||||
buildProfileOverview,
|
buildProfileOverview,
|
||||||
buildStatsEmbed,
|
buildStatsEmbed,
|
||||||
buildBadgesEmbed,
|
buildBadgesEmbed,
|
||||||
|
|||||||
Reference in New Issue
Block a user