updated profile and centralized functions
This commit is contained in:
@@ -1,54 +1,51 @@
|
|||||||
const {
|
const {
|
||||||
|
requireProfileOwner,
|
||||||
buildAchievementsEmbed,
|
buildAchievementsEmbed,
|
||||||
buildProfileButtons,
|
buildProfileButtons,
|
||||||
getProfileContextFromFooter,
|
buildProfilePaginationButtons
|
||||||
requireProfileOwner
|
} = require("../../utils/profileSystem");
|
||||||
} = require(
|
|
||||||
"../../utils/profileSystem"
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name:
|
name: "profileAchievements",
|
||||||
"profileAchievements",
|
|
||||||
|
|
||||||
async execute(
|
async execute(interaction) {
|
||||||
interaction
|
const session = await requireProfileOwner(
|
||||||
) {
|
interaction
|
||||||
const context =
|
);
|
||||||
await requireProfileOwner(
|
|
||||||
interaction
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!context) {
|
if (!session) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user =
|
const user = await interaction.client.users.fetch(
|
||||||
await interaction.client.users.fetch(
|
session.profileUserId
|
||||||
context.profileUserId
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const embed =
|
const result = await buildAchievementsEmbed(
|
||||||
await buildAchievementsEmbed(
|
interaction.guildId,
|
||||||
interaction.guildId,
|
user,
|
||||||
user,
|
0,
|
||||||
context.sessionOwnerId,
|
session.sessionOwnerId
|
||||||
0
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const pageContext =
|
const components = [
|
||||||
getProfileContextFromFooter(
|
...buildProfileButtons()
|
||||||
embed.data.footer?.text
|
];
|
||||||
|
|
||||||
|
if (result.pageCount > 1) {
|
||||||
|
components.push(
|
||||||
|
buildProfilePaginationButtons(
|
||||||
|
"profileAchievementsLeft",
|
||||||
|
"profileAchievementsRight",
|
||||||
|
result.page,
|
||||||
|
result.pageCount
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [embed],
|
embeds: [result.embed],
|
||||||
components:
|
components
|
||||||
buildProfileButtons(
|
|
||||||
"achievements",
|
|
||||||
pageContext?.page ?? 0,
|
|
||||||
pageContext?.pageCount ?? 1
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,54 +1,53 @@
|
|||||||
|
// buttons/profile/achievementsLeft.js
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
requireProfileOwner,
|
||||||
buildAchievementsEmbed,
|
buildAchievementsEmbed,
|
||||||
buildProfileButtons,
|
buildProfileButtons,
|
||||||
getProfileContextFromFooter,
|
buildProfilePaginationButtons
|
||||||
requireProfileOwner
|
} = require("../../utils/profileSystem");
|
||||||
} = require(
|
|
||||||
"../../utils/profileSystem"
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name:
|
name: "profileAchievementsLeft",
|
||||||
"profileAchievementsLeft",
|
|
||||||
|
|
||||||
async execute(
|
async execute(interaction) {
|
||||||
interaction
|
const session = await requireProfileOwner(
|
||||||
) {
|
interaction
|
||||||
const context =
|
);
|
||||||
await requireProfileOwner(
|
|
||||||
interaction
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!context) {
|
if (!session) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user =
|
const user = await interaction.client.users.fetch(
|
||||||
await interaction.client.users.fetch(
|
session.profileUserId
|
||||||
context.profileUserId
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const embed =
|
const result = await buildAchievementsEmbed(
|
||||||
await buildAchievementsEmbed(
|
interaction.guildId,
|
||||||
interaction.guildId,
|
user,
|
||||||
user,
|
session.page - 1,
|
||||||
context.sessionOwnerId,
|
session.sessionOwnerId
|
||||||
context.page - 1
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const pageContext =
|
const components = [
|
||||||
getProfileContextFromFooter(
|
...buildProfileButtons()
|
||||||
embed.data.footer?.text
|
];
|
||||||
|
|
||||||
|
if (result.pageCount > 1) {
|
||||||
|
components.push(
|
||||||
|
buildProfilePaginationButtons(
|
||||||
|
"profileAchievementsLeft",
|
||||||
|
"profileAchievementsRight",
|
||||||
|
result.page,
|
||||||
|
result.pageCount
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [embed],
|
embeds: [result.embed],
|
||||||
components:
|
components
|
||||||
buildProfileButtons(
|
|
||||||
"achievements",
|
|
||||||
pageContext?.page ?? 0,
|
|
||||||
pageContext?.pageCount ?? 1
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,54 +1,53 @@
|
|||||||
|
// buttons/profile/achievementsRight.js
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
requireProfileOwner,
|
||||||
buildAchievementsEmbed,
|
buildAchievementsEmbed,
|
||||||
buildProfileButtons,
|
buildProfileButtons,
|
||||||
getProfileContextFromFooter,
|
buildProfilePaginationButtons
|
||||||
requireProfileOwner
|
} = require("../../utils/profileSystem");
|
||||||
} = require(
|
|
||||||
"../../utils/profileSystem"
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name:
|
name: "profileAchievementsRight",
|
||||||
"profileAchievementsRight",
|
|
||||||
|
|
||||||
async execute(
|
async execute(interaction) {
|
||||||
interaction
|
const session = await requireProfileOwner(
|
||||||
) {
|
interaction
|
||||||
const context =
|
);
|
||||||
await requireProfileOwner(
|
|
||||||
interaction
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!context) {
|
if (!session) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user =
|
const user = await interaction.client.users.fetch(
|
||||||
await interaction.client.users.fetch(
|
session.profileUserId
|
||||||
context.profileUserId
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const embed =
|
const result = await buildAchievementsEmbed(
|
||||||
await buildAchievementsEmbed(
|
interaction.guildId,
|
||||||
interaction.guildId,
|
user,
|
||||||
user,
|
session.page + 1,
|
||||||
context.sessionOwnerId,
|
session.sessionOwnerId
|
||||||
context.page + 1
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const pageContext =
|
const components = [
|
||||||
getProfileContextFromFooter(
|
...buildProfileButtons()
|
||||||
embed.data.footer?.text
|
];
|
||||||
|
|
||||||
|
if (result.pageCount > 1) {
|
||||||
|
components.push(
|
||||||
|
buildProfilePaginationButtons(
|
||||||
|
"profileAchievementsLeft",
|
||||||
|
"profileAchievementsRight",
|
||||||
|
result.page,
|
||||||
|
result.pageCount
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [embed],
|
embeds: [result.embed],
|
||||||
components:
|
components
|
||||||
buildProfileButtons(
|
|
||||||
"achievements",
|
|
||||||
pageContext?.page ?? 0,
|
|
||||||
pageContext?.pageCount ?? 1
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
+34
-37
@@ -1,54 +1,51 @@
|
|||||||
const {
|
const {
|
||||||
|
requireProfileOwner,
|
||||||
buildBadgesEmbed,
|
buildBadgesEmbed,
|
||||||
buildProfileButtons,
|
buildProfileButtons,
|
||||||
getProfileContextFromFooter,
|
buildProfilePaginationButtons
|
||||||
requireProfileOwner
|
} = require("../../utils/profileSystem");
|
||||||
} = require(
|
|
||||||
"../../utils/profileSystem"
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name:
|
name: "profileBadges",
|
||||||
"profileBadges",
|
|
||||||
|
|
||||||
async execute(
|
async execute(interaction) {
|
||||||
interaction
|
const session = await requireProfileOwner(
|
||||||
) {
|
interaction
|
||||||
const context =
|
);
|
||||||
await requireProfileOwner(
|
|
||||||
interaction
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!context) {
|
if (!session) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user =
|
const user = await interaction.client.users.fetch(
|
||||||
await interaction.client.users.fetch(
|
session.profileUserId
|
||||||
context.profileUserId
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const embed =
|
const result = await buildBadgesEmbed(
|
||||||
await buildBadgesEmbed(
|
interaction.guildId,
|
||||||
interaction.guildId,
|
user,
|
||||||
user,
|
0,
|
||||||
context.sessionOwnerId,
|
session.sessionOwnerId
|
||||||
0
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const pageContext =
|
const components = [
|
||||||
getProfileContextFromFooter(
|
...buildProfileButtons()
|
||||||
embed.data.footer?.text
|
];
|
||||||
|
|
||||||
|
if (result.pageCount > 1) {
|
||||||
|
components.push(
|
||||||
|
buildProfilePaginationButtons(
|
||||||
|
"profileBadgesLeft",
|
||||||
|
"profileBadgesRight",
|
||||||
|
result.page,
|
||||||
|
result.pageCount
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [embed],
|
embeds: [result.embed],
|
||||||
components:
|
components
|
||||||
buildProfileButtons(
|
|
||||||
"badges",
|
|
||||||
pageContext?.page ?? 0,
|
|
||||||
pageContext?.pageCount ?? 1
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,54 +1,53 @@
|
|||||||
|
// buttons/profile/badgesLeft.js
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
requireProfileOwner,
|
||||||
buildBadgesEmbed,
|
buildBadgesEmbed,
|
||||||
buildProfileButtons,
|
buildProfileButtons,
|
||||||
getProfileContextFromFooter,
|
buildProfilePaginationButtons
|
||||||
requireProfileOwner
|
} = require("../../utils/profileSystem");
|
||||||
} = require(
|
|
||||||
"../../utils/profileSystem"
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name:
|
name: "profileBadgesLeft",
|
||||||
"profileBadgesLeft",
|
|
||||||
|
|
||||||
async execute(
|
async execute(interaction) {
|
||||||
interaction
|
const session = await requireProfileOwner(
|
||||||
) {
|
interaction
|
||||||
const context =
|
);
|
||||||
await requireProfileOwner(
|
|
||||||
interaction
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!context) {
|
if (!session) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user =
|
const user = await interaction.client.users.fetch(
|
||||||
await interaction.client.users.fetch(
|
session.profileUserId
|
||||||
context.profileUserId
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const embed =
|
const result = await buildBadgesEmbed(
|
||||||
await buildBadgesEmbed(
|
interaction.guildId,
|
||||||
interaction.guildId,
|
user,
|
||||||
user,
|
session.page - 1,
|
||||||
context.sessionOwnerId,
|
session.sessionOwnerId
|
||||||
context.page - 1
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const pageContext =
|
const components = [
|
||||||
getProfileContextFromFooter(
|
...buildProfileButtons()
|
||||||
embed.data.footer?.text
|
];
|
||||||
|
|
||||||
|
if (result.pageCount > 1) {
|
||||||
|
components.push(
|
||||||
|
buildProfilePaginationButtons(
|
||||||
|
"profileBadgesLeft",
|
||||||
|
"profileBadgesRight",
|
||||||
|
result.page,
|
||||||
|
result.pageCount
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [embed],
|
embeds: [result.embed],
|
||||||
components:
|
components
|
||||||
buildProfileButtons(
|
|
||||||
"badges",
|
|
||||||
pageContext?.page ?? 0,
|
|
||||||
pageContext?.pageCount ?? 1
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,54 +1,53 @@
|
|||||||
|
// buttons/profile/badgesRight.js
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
requireProfileOwner,
|
||||||
buildBadgesEmbed,
|
buildBadgesEmbed,
|
||||||
buildProfileButtons,
|
buildProfileButtons,
|
||||||
getProfileContextFromFooter,
|
buildProfilePaginationButtons
|
||||||
requireProfileOwner
|
} = require("../../utils/profileSystem");
|
||||||
} = require(
|
|
||||||
"../../utils/profileSystem"
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name:
|
name: "profileBadgesRight",
|
||||||
"profileBadgesRight",
|
|
||||||
|
|
||||||
async execute(
|
async execute(interaction) {
|
||||||
interaction
|
const session = await requireProfileOwner(
|
||||||
) {
|
interaction
|
||||||
const context =
|
);
|
||||||
await requireProfileOwner(
|
|
||||||
interaction
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!context) {
|
if (!session) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user =
|
const user = await interaction.client.users.fetch(
|
||||||
await interaction.client.users.fetch(
|
session.profileUserId
|
||||||
context.profileUserId
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const embed =
|
const result = await buildBadgesEmbed(
|
||||||
await buildBadgesEmbed(
|
interaction.guildId,
|
||||||
interaction.guildId,
|
user,
|
||||||
user,
|
session.page + 1,
|
||||||
context.sessionOwnerId,
|
session.sessionOwnerId
|
||||||
context.page + 1
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const pageContext =
|
const components = [
|
||||||
getProfileContextFromFooter(
|
...buildProfileButtons()
|
||||||
embed.data.footer?.text
|
];
|
||||||
|
|
||||||
|
if (result.pageCount > 1) {
|
||||||
|
components.push(
|
||||||
|
buildProfilePaginationButtons(
|
||||||
|
"profileBadgesLeft",
|
||||||
|
"profileBadgesRight",
|
||||||
|
result.page,
|
||||||
|
result.pageCount
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [embed],
|
embeds: [result.embed],
|
||||||
components:
|
components
|
||||||
buildProfileButtons(
|
|
||||||
"badges",
|
|
||||||
pageContext?.page ?? 0,
|
|
||||||
pageContext?.pageCount ?? 1
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
+20
-33
@@ -1,47 +1,34 @@
|
|||||||
const {
|
const {
|
||||||
buildStatsEmbed,
|
requireProfileOwner,
|
||||||
buildProfileButtons,
|
buildProfileStatsEmbed,
|
||||||
requireProfileOwner
|
buildProfileButtons
|
||||||
} = require(
|
} = require("../../utils/profileSystem");
|
||||||
"../../utils/profileSystem"
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name:
|
name: "profileStats",
|
||||||
"profileStats",
|
|
||||||
|
|
||||||
async execute(
|
async execute(interaction) {
|
||||||
interaction
|
const session = await requireProfileOwner(
|
||||||
) {
|
interaction
|
||||||
const context =
|
);
|
||||||
await requireProfileOwner(
|
|
||||||
interaction
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!context) {
|
if (!session) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user =
|
const user = await interaction.client.users.fetch(
|
||||||
await interaction.client.users.fetch(
|
session.profileUserId
|
||||||
context.profileUserId
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const embed =
|
const embed = await buildProfileStatsEmbed(
|
||||||
await buildStatsEmbed(
|
interaction.guildId,
|
||||||
interaction.guildId,
|
user,
|
||||||
user,
|
session.sessionOwnerId
|
||||||
context.sessionOwnerId
|
);
|
||||||
);
|
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [embed],
|
embeds: [embed],
|
||||||
components:
|
components: buildProfileButtons()
|
||||||
buildProfileButtons(
|
|
||||||
"stats",
|
|
||||||
0,
|
|
||||||
1
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
const {
|
||||||
|
SlashCommandBuilder,
|
||||||
|
MessageFlags
|
||||||
|
} = require(
|
||||||
|
"discord.js"
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
requireAdminAccess
|
||||||
|
} = require(
|
||||||
|
"../../utils/permissions"
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
reconcileProfileAchievements,
|
||||||
|
auditProfileSystem
|
||||||
|
} = require(
|
||||||
|
"../../utils/profileSystem"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================
|
||||||
|
* COMMAND
|
||||||
|
* ============================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data:
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName(
|
||||||
|
"profileadmin"
|
||||||
|
)
|
||||||
|
|
||||||
|
.setDescription(
|
||||||
|
"Profile and achievement administration."
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* =================================================
|
||||||
|
* RECONCILE
|
||||||
|
* =================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
.addSubcommand(
|
||||||
|
subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName(
|
||||||
|
"reconcile"
|
||||||
|
)
|
||||||
|
|
||||||
|
.setDescription(
|
||||||
|
"Re-evaluate stored statistics for achievements."
|
||||||
|
)
|
||||||
|
|
||||||
|
.addUserOption(
|
||||||
|
option =>
|
||||||
|
option
|
||||||
|
.setName(
|
||||||
|
"user"
|
||||||
|
)
|
||||||
|
|
||||||
|
.setDescription(
|
||||||
|
"Only reconcile this user's profile."
|
||||||
|
)
|
||||||
|
|
||||||
|
.setRequired(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
.addBooleanOption(
|
||||||
|
option =>
|
||||||
|
option
|
||||||
|
.setName(
|
||||||
|
"dry-run"
|
||||||
|
)
|
||||||
|
|
||||||
|
.setDescription(
|
||||||
|
"Show what would change without unlocking anything."
|
||||||
|
)
|
||||||
|
|
||||||
|
.setRequired(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* =================================================
|
||||||
|
* AUDIT
|
||||||
|
* =================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
.addSubcommand(
|
||||||
|
subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName(
|
||||||
|
"audit"
|
||||||
|
)
|
||||||
|
|
||||||
|
.setDescription(
|
||||||
|
"Audit the profile and achievement system."
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ========================================================
|
||||||
|
* EXECUTE
|
||||||
|
* ========================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
interaction
|
||||||
|
) {
|
||||||
|
/*
|
||||||
|
* Runtime authorization.
|
||||||
|
*
|
||||||
|
* This intentionally replaces:
|
||||||
|
*
|
||||||
|
* .setDefaultMemberPermissions(
|
||||||
|
* PermissionFlagsBits.Administrator
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* so BOT_AUTHOR_ID can also invoke the command.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (
|
||||||
|
!await requireAdminAccess(
|
||||||
|
interaction
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const subcommand =
|
||||||
|
interaction.options
|
||||||
|
.getSubcommand();
|
||||||
|
|
||||||
|
|
||||||
|
switch (
|
||||||
|
subcommand
|
||||||
|
) {
|
||||||
|
case "reconcile":
|
||||||
|
|
||||||
|
return runReconcile(
|
||||||
|
interaction
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
case "audit":
|
||||||
|
|
||||||
|
return runAudit(
|
||||||
|
interaction
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
default:
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
content:
|
||||||
|
"Unknown profile administration action.",
|
||||||
|
|
||||||
|
flags:
|
||||||
|
MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================
|
||||||
|
* RECONCILE
|
||||||
|
* ============================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function runReconcile(
|
||||||
|
interaction
|
||||||
|
) {
|
||||||
|
const target =
|
||||||
|
interaction.options.getUser(
|
||||||
|
"user"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const dryRun =
|
||||||
|
interaction.options.getBoolean(
|
||||||
|
"dry-run"
|
||||||
|
)
|
||||||
|
??
|
||||||
|
false;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This can touch multiple Mongo documents,
|
||||||
|
* so acknowledge the interaction first.
|
||||||
|
*/
|
||||||
|
|
||||||
|
await interaction.deferReply({
|
||||||
|
flags:
|
||||||
|
MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result =
|
||||||
|
await reconcileProfileAchievements(
|
||||||
|
interaction.guildId,
|
||||||
|
{
|
||||||
|
userId:
|
||||||
|
target?.id
|
||||||
|
??
|
||||||
|
null,
|
||||||
|
|
||||||
|
dryRun
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
dryRun
|
||||||
|
? "### 🔎 Profile Reconciliation — Dry Run"
|
||||||
|
: "### ✅ Profile Reconciliation Complete",
|
||||||
|
|
||||||
|
"",
|
||||||
|
|
||||||
|
target
|
||||||
|
? `Target: ${target}`
|
||||||
|
: "Target: **Entire Guild**",
|
||||||
|
|
||||||
|
"",
|
||||||
|
|
||||||
|
`Profiles checked: **${result.profilesChecked ?? 0}**`,
|
||||||
|
`Achievement unlocks found: **${result.unlocksFound ?? 0}**`
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
!dryRun
|
||||||
|
) {
|
||||||
|
lines.push(
|
||||||
|
`Achievements unlocked: **${result.unlocksApplied ?? 0}**`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
result.skippedEventOnly !==
|
||||||
|
undefined
|
||||||
|
) {
|
||||||
|
lines.push(
|
||||||
|
`Event-only checks skipped: **${result.skippedEventOnly}**`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
Array.isArray(
|
||||||
|
result.details
|
||||||
|
)
|
||||||
|
&&
|
||||||
|
result.details.length >
|
||||||
|
0
|
||||||
|
) {
|
||||||
|
const preview =
|
||||||
|
result.details
|
||||||
|
.slice(
|
||||||
|
0,
|
||||||
|
15
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
entry => {
|
||||||
|
if (
|
||||||
|
typeof entry ===
|
||||||
|
"string"
|
||||||
|
) {
|
||||||
|
return `• ${entry}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
`• <@${entry.userId}> — ` +
|
||||||
|
`${entry.achievementName ?? entry.achievementId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
lines.push(
|
||||||
|
"",
|
||||||
|
"### Changes",
|
||||||
|
...preview
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
result.details.length >
|
||||||
|
preview.length
|
||||||
|
) {
|
||||||
|
lines.push(
|
||||||
|
"",
|
||||||
|
`*...and ${result.details.length - preview.length} more.*`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return interaction.editReply({
|
||||||
|
content:
|
||||||
|
lines.join(
|
||||||
|
"\n"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[ProfileAdmin] Reconciliation failed:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return interaction.editReply({
|
||||||
|
content:
|
||||||
|
"Profile reconciliation failed. Check the bot logs for details."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================
|
||||||
|
* AUDIT
|
||||||
|
* ============================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function runAudit(
|
||||||
|
interaction
|
||||||
|
) {
|
||||||
|
await interaction.deferReply({
|
||||||
|
flags:
|
||||||
|
MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result =
|
||||||
|
await auditProfileSystem(
|
||||||
|
interaction.guildId
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
"### 🧾 Profile System Audit",
|
||||||
|
"",
|
||||||
|
|
||||||
|
`Profile documents: **${result.profileDocuments ?? 0}**`,
|
||||||
|
`Achievement documents: **${result.achievementDocuments ?? 0}**`,
|
||||||
|
`Active achievement definitions: **${result.activeAchievements ?? 0}**`,
|
||||||
|
`Legacy achievement definitions: **${result.legacyAchievements ?? 0}**`,
|
||||||
|
`Guild badges: **${result.badges ?? 0}**`,
|
||||||
|
`Claimed badges: **${result.claimedBadges ?? 0}**`,
|
||||||
|
`Available badges: **${result.availableBadges ?? 0}**`
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
Array.isArray(
|
||||||
|
result.warnings
|
||||||
|
)
|
||||||
|
&&
|
||||||
|
result.warnings.length >
|
||||||
|
0
|
||||||
|
) {
|
||||||
|
lines.push(
|
||||||
|
"",
|
||||||
|
"### ⚠️ Warnings",
|
||||||
|
|
||||||
|
...result.warnings.map(
|
||||||
|
warning =>
|
||||||
|
`• ${warning}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
lines.push(
|
||||||
|
"",
|
||||||
|
"✅ No obvious profile-system issues were found."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return interaction.editReply({
|
||||||
|
content:
|
||||||
|
lines.join(
|
||||||
|
"\n"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[ProfileAdmin] Audit failed:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return interaction.editReply({
|
||||||
|
content:
|
||||||
|
"Profile audit failed. Check the bot logs for details."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
const {
|
||||||
|
SlashCommandBuilder
|
||||||
|
} = require("discord.js");
|
||||||
|
|
||||||
|
const {
|
||||||
|
buildRecordsEmbed,
|
||||||
|
getRecordChoices
|
||||||
|
} = require(
|
||||||
|
"../../utils/profileRecords"
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data:
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName(
|
||||||
|
"records"
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
"View Shai Cookie and Crumb Saucer records"
|
||||||
|
)
|
||||||
|
.addStringOption(
|
||||||
|
option =>
|
||||||
|
option
|
||||||
|
.setName(
|
||||||
|
"category"
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
"Record category"
|
||||||
|
)
|
||||||
|
.setRequired(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
.addChoices(
|
||||||
|
...getRecordChoices()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
interaction
|
||||||
|
) {
|
||||||
|
const category =
|
||||||
|
interaction.options.getString(
|
||||||
|
"category",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const embed =
|
||||||
|
await buildRecordsEmbed({
|
||||||
|
guildId:
|
||||||
|
interaction.guildId,
|
||||||
|
category,
|
||||||
|
botUserId:
|
||||||
|
interaction.client.user.id,
|
||||||
|
limit: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [embed]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
const {
|
const {
|
||||||
Client,
|
Client,
|
||||||
GatewayIntentBits,
|
GatewayIntentBits,
|
||||||
Collection,
|
Collection,
|
||||||
Partials
|
Partials
|
||||||
} = require("discord.js");
|
} = require("discord.js");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
MongoClient
|
MongoClient
|
||||||
} = require("mongodb");
|
} = require("mongodb");
|
||||||
|
|
||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
@@ -19,12 +19,12 @@ require("dotenv").config();
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const mClient =
|
const mClient =
|
||||||
new MongoClient(
|
new MongoClient(
|
||||||
process.env.M_URI
|
process.env.M_URI
|
||||||
);
|
);
|
||||||
|
|
||||||
exports.mClient =
|
exports.mClient =
|
||||||
mClient;
|
mClient;
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -32,30 +32,29 @@ exports.mClient =
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const client =
|
const client =
|
||||||
new Client({
|
new Client({
|
||||||
intents: [
|
intents: [
|
||||||
GatewayIntentBits.Guilds,
|
GatewayIntentBits.Guilds,
|
||||||
GatewayIntentBits.GuildMembers,
|
GatewayIntentBits.GuildMembers,
|
||||||
GatewayIntentBits.GuildMessages,
|
GatewayIntentBits.GuildMessages,
|
||||||
GatewayIntentBits.GuildPresences,
|
GatewayIntentBits.GuildPresences,
|
||||||
GatewayIntentBits.GuildMessageReactions,
|
GatewayIntentBits.GuildMessageReactions,
|
||||||
GatewayIntentBits.GuildVoiceStates,
|
GatewayIntentBits.GuildVoiceStates,
|
||||||
GatewayIntentBits.DirectMessages,
|
GatewayIntentBits.DirectMessages,
|
||||||
GatewayIntentBits.MessageContent
|
GatewayIntentBits.MessageContent
|
||||||
],
|
],
|
||||||
|
|
||||||
partials: [
|
|
||||||
Partials.Channel,
|
|
||||||
Partials.Message,
|
|
||||||
Partials.User,
|
|
||||||
Partials.GuildMember,
|
|
||||||
Partials.Reaction
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
|
partials: [
|
||||||
|
Partials.Channel,
|
||||||
|
Partials.Message,
|
||||||
|
Partials.User,
|
||||||
|
Partials.GuildMember,
|
||||||
|
Partials.Reaction
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
exports.client =
|
exports.client =
|
||||||
client;
|
client;
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -63,22 +62,22 @@ exports.client =
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
client.commands =
|
client.commands =
|
||||||
new Collection();
|
new Collection();
|
||||||
|
|
||||||
client.legacyCommands =
|
client.legacyCommands =
|
||||||
new Collection();
|
new Collection();
|
||||||
|
|
||||||
client.aliases =
|
client.aliases =
|
||||||
new Collection();
|
new Collection();
|
||||||
|
|
||||||
client.buttons =
|
client.buttons =
|
||||||
new Collection();
|
new Collection();
|
||||||
|
|
||||||
client.selectMenus =
|
client.selectMenus =
|
||||||
new Collection();
|
new Collection();
|
||||||
|
|
||||||
client.modals =
|
client.modals =
|
||||||
new Collection();
|
new Collection();
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -86,56 +85,84 @@ client.modals =
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
try {
|
try {
|
||||||
|
/*
|
||||||
|
* Connect Mongo first.
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
await mClient.connect();
|
||||||
* Connect Mongo first.
|
|
||||||
*/
|
|
||||||
|
|
||||||
await mClient.connect();
|
console.log(
|
||||||
|
"MongoDB connected"
|
||||||
console.log(
|
);
|
||||||
"MongoDB connected"
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Load handlers.
|
* 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 handlerFiles =
|
const {
|
||||||
fs.readdirSync(
|
ensureCookieIndexes
|
||||||
"./handlers"
|
} = require(
|
||||||
);
|
"./utils/cookieEconomy"
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
ensureProfileSystem
|
||||||
|
} = require(
|
||||||
|
"./utils/profileSystem"
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
ensureCookieIndexes(),
|
||||||
|
ensureProfileSystem()
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"Database indexes initialized"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
for (
|
/*
|
||||||
const handler
|
* Load handlers.
|
||||||
of handlerFiles
|
*/
|
||||||
) {
|
|
||||||
require(
|
const handlerFiles =
|
||||||
`./handlers/${handler}`
|
fs.readdirSync(
|
||||||
)(client);
|
"./handlers"
|
||||||
}
|
);
|
||||||
|
|
||||||
|
for (
|
||||||
|
const handler
|
||||||
|
of handlerFiles
|
||||||
|
) {
|
||||||
|
require(
|
||||||
|
`./handlers/${handler}`
|
||||||
|
)(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Login Discord.
|
* 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:",
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
start();
|
||||||
start();
|
|
||||||
|
|||||||
+4
-26
@@ -15,7 +15,8 @@ const {
|
|||||||
} = require("./cookieEconomy");
|
} = require("./cookieEconomy");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
trackProfileEvent
|
trackProfileEvent,
|
||||||
|
recordGameOutcome
|
||||||
} = require("./profileSystem");
|
} = require("./profileSystem");
|
||||||
|
|
||||||
const HEIST_COOLDOWN =
|
const HEIST_COOLDOWN =
|
||||||
@@ -255,12 +256,10 @@ async function resolveCookieHeist(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
await trackProfileEvent(
|
await recordGameOutcome(
|
||||||
interaction,
|
interaction,
|
||||||
playerId,
|
playerId,
|
||||||
{
|
{
|
||||||
type:
|
|
||||||
"game_result",
|
|
||||||
game:
|
game:
|
||||||
"heist",
|
"heist",
|
||||||
result:
|
result:
|
||||||
@@ -280,15 +279,6 @@ async function resolveCookieHeist(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await trackProfileEvent(
|
|
||||||
interaction,
|
|
||||||
playerId,
|
|
||||||
{
|
|
||||||
type:
|
|
||||||
"cookie_gain",
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
playerAfter,
|
playerAfter,
|
||||||
@@ -444,12 +434,10 @@ async function resolveCookieHeist(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
await trackProfileEvent(
|
await recordGameOutcome(
|
||||||
interaction,
|
interaction,
|
||||||
playerId,
|
playerId,
|
||||||
{
|
{
|
||||||
type:
|
|
||||||
"game_result",
|
|
||||||
game:
|
game:
|
||||||
"heist",
|
"heist",
|
||||||
result:
|
result:
|
||||||
@@ -459,16 +447,6 @@ async function resolveCookieHeist(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await trackProfileEvent(
|
|
||||||
interaction,
|
|
||||||
playerId,
|
|
||||||
{
|
|
||||||
type:
|
|
||||||
"cookie_loss",
|
|
||||||
amount:
|
|
||||||
penalty
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
playerAfter,
|
playerAfter,
|
||||||
|
|||||||
+2
-30
@@ -18,7 +18,7 @@ const {
|
|||||||
} = require("./cookieEconomy");
|
} = require("./cookieEconomy");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
trackProfileEvent
|
recordGameOutcome
|
||||||
} = require("./profileSystem");
|
} = require("./profileSystem");
|
||||||
|
|
||||||
const GAME_COLLECTION =
|
const GAME_COLLECTION =
|
||||||
@@ -929,12 +929,10 @@ async function finishBlackjack(
|
|||||||
profileResult = "loss";
|
profileResult = "loss";
|
||||||
}
|
}
|
||||||
|
|
||||||
await trackProfileEvent(
|
await recordGameOutcome(
|
||||||
interaction,
|
interaction,
|
||||||
game.userId,
|
game.userId,
|
||||||
{
|
{
|
||||||
type:
|
|
||||||
"game_result",
|
|
||||||
game:
|
game:
|
||||||
"blackjack",
|
"blackjack",
|
||||||
result:
|
result:
|
||||||
@@ -949,32 +947,6 @@ async function finishBlackjack(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (profit > 0) {
|
|
||||||
await trackProfileEvent(
|
|
||||||
interaction,
|
|
||||||
game.userId,
|
|
||||||
{
|
|
||||||
type:
|
|
||||||
"cookie_gain",
|
|
||||||
amount:
|
|
||||||
profit
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (profit < 0) {
|
|
||||||
await trackProfileEvent(
|
|
||||||
interaction,
|
|
||||||
game.userId,
|
|
||||||
{
|
|
||||||
type:
|
|
||||||
"cookie_loss",
|
|
||||||
amount:
|
|
||||||
Math.abs(profit)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const description = [
|
const description = [
|
||||||
"**Dealer**",
|
"**Dealer**",
|
||||||
formatCards(
|
formatCards(
|
||||||
|
|||||||
+2
-30
@@ -12,7 +12,7 @@ const {
|
|||||||
} = require("./cookieEconomy");
|
} = require("./cookieEconomy");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
trackProfileEvent
|
recordGameOutcome
|
||||||
} = require("./profileSystem");
|
} = require("./profileSystem");
|
||||||
|
|
||||||
const ROULETTE_BETS = [
|
const ROULETTE_BETS = [
|
||||||
@@ -577,12 +577,10 @@ async function playRoulette(
|
|||||||
const profit =
|
const profit =
|
||||||
payout - bet;
|
payout - bet;
|
||||||
|
|
||||||
await trackProfileEvent(
|
await recordGameOutcome(
|
||||||
interaction,
|
interaction,
|
||||||
playerId,
|
playerId,
|
||||||
{
|
{
|
||||||
type:
|
|
||||||
"game_result",
|
|
||||||
game:
|
game:
|
||||||
"roulette",
|
"roulette",
|
||||||
result:
|
result:
|
||||||
@@ -602,32 +600,6 @@ async function playRoulette(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (profit > 0) {
|
|
||||||
await trackProfileEvent(
|
|
||||||
interaction,
|
|
||||||
playerId,
|
|
||||||
{
|
|
||||||
type:
|
|
||||||
"cookie_gain",
|
|
||||||
amount:
|
|
||||||
profit
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (profit < 0) {
|
|
||||||
await trackProfileEvent(
|
|
||||||
interaction,
|
|
||||||
playerId,
|
|
||||||
{
|
|
||||||
type:
|
|
||||||
"cookie_loss",
|
|
||||||
amount:
|
|
||||||
Math.abs(profit)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultEmbed =
|
const resultEmbed =
|
||||||
new EmbedBuilder()
|
new EmbedBuilder()
|
||||||
.setTitle(
|
.setTitle(
|
||||||
|
|||||||
+2
-30
@@ -13,7 +13,7 @@ const {
|
|||||||
} = require("./cookieEconomy");
|
} = require("./cookieEconomy");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
trackProfileEvent
|
recordGameOutcome
|
||||||
} = require("./profileSystem");
|
} = require("./profileSystem");
|
||||||
|
|
||||||
const SLOT_BETS = [
|
const SLOT_BETS = [
|
||||||
@@ -308,12 +308,10 @@ async function playSlots(
|
|||||||
const profit =
|
const profit =
|
||||||
payout - bet;
|
payout - bet;
|
||||||
|
|
||||||
await trackProfileEvent(
|
await recordGameOutcome(
|
||||||
interaction,
|
interaction,
|
||||||
playerId,
|
playerId,
|
||||||
{
|
{
|
||||||
type:
|
|
||||||
"game_result",
|
|
||||||
game:
|
game:
|
||||||
"slots",
|
"slots",
|
||||||
result:
|
result:
|
||||||
@@ -331,32 +329,6 @@ async function playSlots(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (profit > 0) {
|
|
||||||
await trackProfileEvent(
|
|
||||||
interaction,
|
|
||||||
playerId,
|
|
||||||
{
|
|
||||||
type:
|
|
||||||
"cookie_gain",
|
|
||||||
amount:
|
|
||||||
profit
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (profit < 0) {
|
|
||||||
await trackProfileEvent(
|
|
||||||
interaction,
|
|
||||||
playerId,
|
|
||||||
{
|
|
||||||
type:
|
|
||||||
"cookie_loss",
|
|
||||||
amount:
|
|
||||||
Math.abs(profit)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultEmbed =
|
const resultEmbed =
|
||||||
new EmbedBuilder()
|
new EmbedBuilder()
|
||||||
.setTitle(
|
.setTitle(
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
const {
|
||||||
|
PermissionFlagsBits,
|
||||||
|
MessageFlags
|
||||||
|
} = require(
|
||||||
|
"discord.js"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================
|
||||||
|
* CONFIG
|
||||||
|
* ============================================================
|
||||||
|
*
|
||||||
|
* .env:
|
||||||
|
*
|
||||||
|
* BOT_AUTHOR_ID=123456789012345678
|
||||||
|
*
|
||||||
|
* Do not hard-code the author ID into commands.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getBotAuthorId() {
|
||||||
|
return (
|
||||||
|
process.env.BOT_AUTHOR_ID
|
||||||
|
??
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================
|
||||||
|
* BOT AUTHOR
|
||||||
|
* ============================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
function isBotAuthor(
|
||||||
|
userId
|
||||||
|
) {
|
||||||
|
const authorId =
|
||||||
|
getBotAuthorId();
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
!authorId
|
||||||
|
||
|
||||||
|
!userId
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
String(
|
||||||
|
userId
|
||||||
|
)
|
||||||
|
===
|
||||||
|
String(
|
||||||
|
authorId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================
|
||||||
|
* GUILD ADMINISTRATOR
|
||||||
|
* ============================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
function isAdministrator(
|
||||||
|
interaction
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!interaction
|
||||||
|
?.memberPermissions
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return interaction
|
||||||
|
.memberPermissions
|
||||||
|
.has(
|
||||||
|
PermissionFlagsBits.Administrator
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================
|
||||||
|
* 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(
|
||||||
|
interaction
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!interaction
|
||||||
|
?.user
|
||||||
|
?.id
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
isBotAuthor(
|
||||||
|
interaction.user.id
|
||||||
|
)
|
||||||
|
||
|
||||||
|
isAdministrator(
|
||||||
|
interaction
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================
|
||||||
|
* 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(
|
||||||
|
interaction
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
canUseAdminCommands(
|
||||||
|
interaction
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
await denyAdminAccess(
|
||||||
|
interaction
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================
|
||||||
|
* EXPORTS
|
||||||
|
* ============================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getBotAuthorId,
|
||||||
|
|
||||||
|
isBotAuthor,
|
||||||
|
isAdministrator,
|
||||||
|
|
||||||
|
canUseAdminCommands,
|
||||||
|
requireAdminAccess,
|
||||||
|
|
||||||
|
denyAdminAccess
|
||||||
|
};
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
const {
|
||||||
|
getProfileCollections,
|
||||||
|
getProfileStats,
|
||||||
|
readStatValue
|
||||||
|
} = require("./profileSystem");
|
||||||
|
|
||||||
|
const {
|
||||||
|
getUnixTime
|
||||||
|
} = require("./cookieEconomy");
|
||||||
|
|
||||||
|
function getActiveDefinitions(
|
||||||
|
achievementConfig
|
||||||
|
) {
|
||||||
|
return achievementConfig
|
||||||
|
.find({
|
||||||
|
active: {
|
||||||
|
$ne: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reconcileUserAchievements({
|
||||||
|
guildId,
|
||||||
|
userId,
|
||||||
|
dryRun = false
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
achievements,
|
||||||
|
achievementConfig
|
||||||
|
} = getProfileCollections();
|
||||||
|
|
||||||
|
const [
|
||||||
|
statsDocument,
|
||||||
|
definitions,
|
||||||
|
achievementDocument
|
||||||
|
] = await Promise.all([
|
||||||
|
getProfileStats(
|
||||||
|
userId,
|
||||||
|
guildId
|
||||||
|
),
|
||||||
|
getActiveDefinitions(
|
||||||
|
achievementConfig
|
||||||
|
),
|
||||||
|
achievements.findOne({
|
||||||
|
userId,
|
||||||
|
guildId
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const unlocked = {
|
||||||
|
...(achievementDocument?.unlocked ?? {})
|
||||||
|
};
|
||||||
|
|
||||||
|
const additions = {};
|
||||||
|
let skippedEventOnly = 0;
|
||||||
|
|
||||||
|
const normalDefinitions =
|
||||||
|
definitions.filter(
|
||||||
|
definition =>
|
||||||
|
definition.measure !==
|
||||||
|
"achievementPercentage"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const definition of normalDefinitions) {
|
||||||
|
if (unlocked[definition.achievementId]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!definition.stat) {
|
||||||
|
skippedEventOnly++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentValue =
|
||||||
|
readStatValue(
|
||||||
|
statsDocument,
|
||||||
|
definition
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentValue <
|
||||||
|
Number(definition.target ?? 0)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp =
|
||||||
|
getUnixTime();
|
||||||
|
|
||||||
|
additions[
|
||||||
|
definition.achievementId
|
||||||
|
] = timestamp;
|
||||||
|
|
||||||
|
unlocked[
|
||||||
|
definition.achievementId
|
||||||
|
] = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectibleDefinitions =
|
||||||
|
definitions.filter(
|
||||||
|
definition =>
|
||||||
|
definition.measure !==
|
||||||
|
"achievementPercentage"
|
||||||
|
);
|
||||||
|
|
||||||
|
const unlockedCollectible =
|
||||||
|
collectibleDefinitions.filter(
|
||||||
|
definition =>
|
||||||
|
Boolean(
|
||||||
|
unlocked[
|
||||||
|
definition.achievementId
|
||||||
|
]
|
||||||
|
)
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const percentage =
|
||||||
|
collectibleDefinitions.length > 0
|
||||||
|
? unlockedCollectible /
|
||||||
|
collectibleDefinitions.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
for (
|
||||||
|
const definition
|
||||||
|
of definitions.filter(
|
||||||
|
item =>
|
||||||
|
item.measure ===
|
||||||
|
"achievementPercentage"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
unlocked[
|
||||||
|
definition.achievementId
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
percentage <
|
||||||
|
Number(definition.target ?? 0)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
additions[
|
||||||
|
definition.achievementId
|
||||||
|
] = getUnixTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
const additionEntries =
|
||||||
|
Object.entries(additions);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!dryRun &&
|
||||||
|
additionEntries.length > 0
|
||||||
|
) {
|
||||||
|
const set = {};
|
||||||
|
|
||||||
|
for (
|
||||||
|
const [achievementId, timestamp]
|
||||||
|
of additionEntries
|
||||||
|
) {
|
||||||
|
set[
|
||||||
|
`unlocked.${achievementId}`
|
||||||
|
] = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
await achievements.updateOne(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
guildId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$setOnInsert: {
|
||||||
|
userId,
|
||||||
|
guildId
|
||||||
|
},
|
||||||
|
$set: set
|
||||||
|
},
|
||||||
|
{
|
||||||
|
upsert: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
checked:
|
||||||
|
definitions.length,
|
||||||
|
unlocked:
|
||||||
|
additionEntries.length,
|
||||||
|
skippedEventOnly,
|
||||||
|
dryRun,
|
||||||
|
achievementIds:
|
||||||
|
additionEntries.map(
|
||||||
|
([achievementId]) =>
|
||||||
|
achievementId
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reconcileAllAchievements({
|
||||||
|
guildId,
|
||||||
|
dryRun = false
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
stats
|
||||||
|
} = getProfileCollections();
|
||||||
|
|
||||||
|
const userIds =
|
||||||
|
await stats.distinct(
|
||||||
|
"userId",
|
||||||
|
{
|
||||||
|
guildId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const summary = {
|
||||||
|
users:
|
||||||
|
userIds.length,
|
||||||
|
unlocked:
|
||||||
|
0,
|
||||||
|
skippedEventOnly:
|
||||||
|
0,
|
||||||
|
results: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const userId of userIds) {
|
||||||
|
const result =
|
||||||
|
await reconcileUserAchievements({
|
||||||
|
guildId,
|
||||||
|
userId,
|
||||||
|
dryRun
|
||||||
|
});
|
||||||
|
|
||||||
|
summary.unlocked +=
|
||||||
|
result.unlocked;
|
||||||
|
|
||||||
|
summary.skippedEventOnly +=
|
||||||
|
result.skippedEventOnly;
|
||||||
|
|
||||||
|
summary.results.push(
|
||||||
|
result
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function auditProfileData(
|
||||||
|
guildId
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
stats,
|
||||||
|
achievements,
|
||||||
|
achievementConfig,
|
||||||
|
badges
|
||||||
|
} = getProfileCollections();
|
||||||
|
|
||||||
|
const [
|
||||||
|
statDocuments,
|
||||||
|
achievementDocuments,
|
||||||
|
activeAchievements,
|
||||||
|
badgeDocuments
|
||||||
|
] = await Promise.all([
|
||||||
|
stats.find({
|
||||||
|
guildId
|
||||||
|
}).toArray(),
|
||||||
|
achievements.find({
|
||||||
|
guildId
|
||||||
|
}).toArray(),
|
||||||
|
achievementConfig.countDocuments({
|
||||||
|
active: {
|
||||||
|
$ne: false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
badges.find({
|
||||||
|
guildId
|
||||||
|
}).toArray()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const issues = [];
|
||||||
|
|
||||||
|
for (const document of statDocuments) {
|
||||||
|
if (!document.userId) {
|
||||||
|
issues.push(
|
||||||
|
"Stats document missing userId."
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total =
|
||||||
|
document.games?.total ?? {};
|
||||||
|
|
||||||
|
const played =
|
||||||
|
Number(total.played ?? 0);
|
||||||
|
|
||||||
|
const resolved =
|
||||||
|
Number(total.won ?? 0) +
|
||||||
|
Number(total.lost ?? 0) +
|
||||||
|
Number(total.pushed ?? 0);
|
||||||
|
|
||||||
|
if (played !== resolved) {
|
||||||
|
issues.push(
|
||||||
|
`${document.userId}: games.total.played (${played}) does not match W/L/P total (${resolved}).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
const game
|
||||||
|
of [
|
||||||
|
"slots",
|
||||||
|
"blackjack",
|
||||||
|
"roulette",
|
||||||
|
"heist"
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
const value =
|
||||||
|
document.games?.[game] ?? {};
|
||||||
|
|
||||||
|
const gamePlayed =
|
||||||
|
Number(value.played ?? 0);
|
||||||
|
|
||||||
|
const gameResolved =
|
||||||
|
Number(value.won ?? 0) +
|
||||||
|
Number(value.lost ?? 0) +
|
||||||
|
Number(value.pushed ?? 0);
|
||||||
|
|
||||||
|
if (
|
||||||
|
gamePlayed !==
|
||||||
|
gameResolved
|
||||||
|
) {
|
||||||
|
issues.push(
|
||||||
|
`${document.userId}: games.${game}.played (${gamePlayed}) does not match W/L/P total (${gameResolved}).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonNegativeFields = [
|
||||||
|
document.economy?.cookiesObtainedTotal,
|
||||||
|
document.economy?.cookiesLostTotal,
|
||||||
|
document.economy?.cookiesGivenTotal,
|
||||||
|
document.economy?.stolenTotal,
|
||||||
|
document.economy?.stolenFromMeTotal,
|
||||||
|
document.economy?.vaultStolenTotal
|
||||||
|
];
|
||||||
|
|
||||||
|
if (
|
||||||
|
nonNegativeFields.some(
|
||||||
|
value =>
|
||||||
|
Number(value ?? 0) < 0
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
issues.push(
|
||||||
|
`${document.userId}: one or more economy counters are negative.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const statUsers =
|
||||||
|
new Set(
|
||||||
|
statDocuments.map(
|
||||||
|
document =>
|
||||||
|
document.userId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const orphanAchievementDocs =
|
||||||
|
achievementDocuments.filter(
|
||||||
|
document =>
|
||||||
|
!statUsers.has(
|
||||||
|
document.userId
|
||||||
|
)
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const claimedBadges =
|
||||||
|
badgeDocuments.filter(
|
||||||
|
badge =>
|
||||||
|
Boolean(badge.ownerId)
|
||||||
|
).length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
statDocuments:
|
||||||
|
statDocuments.length,
|
||||||
|
achievementDocuments:
|
||||||
|
achievementDocuments.length,
|
||||||
|
activeAchievements,
|
||||||
|
badgeDocuments:
|
||||||
|
badgeDocuments.length,
|
||||||
|
claimedBadges,
|
||||||
|
orphanAchievementDocs,
|
||||||
|
issueCount:
|
||||||
|
issues.length,
|
||||||
|
issues
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
reconcileUserAchievements,
|
||||||
|
reconcileAllAchievements,
|
||||||
|
auditProfileData
|
||||||
|
};
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
const {
|
||||||
|
EmbedBuilder
|
||||||
|
} = require("discord.js");
|
||||||
|
|
||||||
|
const {
|
||||||
|
getProfileCollections
|
||||||
|
} = require("./profileSystem");
|
||||||
|
|
||||||
|
const {
|
||||||
|
getCollections
|
||||||
|
} = require("./cookieEconomy");
|
||||||
|
|
||||||
|
const RECORDS = {
|
||||||
|
balance: {
|
||||||
|
label:
|
||||||
|
"🍪 Current Balance",
|
||||||
|
source:
|
||||||
|
"cookies",
|
||||||
|
field:
|
||||||
|
"cookies",
|
||||||
|
format:
|
||||||
|
value =>
|
||||||
|
`${Number(value ?? 0).toLocaleString()} 🍪`
|
||||||
|
},
|
||||||
|
obtained: {
|
||||||
|
label:
|
||||||
|
"🍪 Lifetime Cookies Obtained",
|
||||||
|
field:
|
||||||
|
"economy.cookiesObtainedTotal",
|
||||||
|
format:
|
||||||
|
value =>
|
||||||
|
`${Number(value ?? 0).toLocaleString()} 🍪`
|
||||||
|
},
|
||||||
|
given: {
|
||||||
|
label:
|
||||||
|
"🎁 Cookies Given",
|
||||||
|
field:
|
||||||
|
"economy.cookiesGivenTotal",
|
||||||
|
format:
|
||||||
|
value =>
|
||||||
|
`${Number(value ?? 0).toLocaleString()} 🍪`
|
||||||
|
},
|
||||||
|
stolen: {
|
||||||
|
label:
|
||||||
|
"🥷 Cookies Stolen",
|
||||||
|
field:
|
||||||
|
"economy.stolenTotal",
|
||||||
|
format:
|
||||||
|
value =>
|
||||||
|
`${Number(value ?? 0).toLocaleString()} 🍪`
|
||||||
|
},
|
||||||
|
vault: {
|
||||||
|
label:
|
||||||
|
"🏦 Bot Vault Stolen",
|
||||||
|
field:
|
||||||
|
"economy.vaultStolenTotal",
|
||||||
|
format:
|
||||||
|
value =>
|
||||||
|
`${Number(value ?? 0).toLocaleString()} 🍪`
|
||||||
|
},
|
||||||
|
wins: {
|
||||||
|
label:
|
||||||
|
"🏆 Games Won",
|
||||||
|
field:
|
||||||
|
"games.total.won",
|
||||||
|
format:
|
||||||
|
value =>
|
||||||
|
Number(value ?? 0).toLocaleString()
|
||||||
|
},
|
||||||
|
played: {
|
||||||
|
label:
|
||||||
|
"🎮 Games Played",
|
||||||
|
field:
|
||||||
|
"games.total.played",
|
||||||
|
format:
|
||||||
|
value =>
|
||||||
|
Number(value ?? 0).toLocaleString()
|
||||||
|
},
|
||||||
|
streak: {
|
||||||
|
label:
|
||||||
|
"🔥 Best Win Streak",
|
||||||
|
field:
|
||||||
|
"streaks.bestWin",
|
||||||
|
format:
|
||||||
|
value =>
|
||||||
|
Number(value ?? 0).toLocaleString()
|
||||||
|
},
|
||||||
|
lost: {
|
||||||
|
label:
|
||||||
|
"💸 Lifetime Cookies Lost",
|
||||||
|
field:
|
||||||
|
"economy.cookiesLostTotal",
|
||||||
|
format:
|
||||||
|
value =>
|
||||||
|
`${Number(value ?? 0).toLocaleString()} 🍪`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getNestedValue(
|
||||||
|
document,
|
||||||
|
path
|
||||||
|
) {
|
||||||
|
return path
|
||||||
|
.split(".")
|
||||||
|
.reduce(
|
||||||
|
(value, key) =>
|
||||||
|
value?.[key],
|
||||||
|
document
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildRecordsEmbed({
|
||||||
|
guildId,
|
||||||
|
category,
|
||||||
|
botUserId,
|
||||||
|
limit = 10
|
||||||
|
}) {
|
||||||
|
const definition =
|
||||||
|
RECORDS[category] ??
|
||||||
|
RECORDS.balance;
|
||||||
|
|
||||||
|
let rows;
|
||||||
|
|
||||||
|
if (
|
||||||
|
definition.source ===
|
||||||
|
"cookies"
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
cookies
|
||||||
|
} = getCollections();
|
||||||
|
|
||||||
|
rows =
|
||||||
|
await cookies
|
||||||
|
.find({
|
||||||
|
userId: {
|
||||||
|
$ne: botUserId
|
||||||
|
},
|
||||||
|
cookies: {
|
||||||
|
$gt: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort({
|
||||||
|
cookies: -1
|
||||||
|
})
|
||||||
|
.limit(limit)
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
const {
|
||||||
|
stats
|
||||||
|
} = getProfileCollections();
|
||||||
|
|
||||||
|
rows =
|
||||||
|
await stats
|
||||||
|
.find({
|
||||||
|
guildId,
|
||||||
|
userId: {
|
||||||
|
$ne: botUserId
|
||||||
|
},
|
||||||
|
[definition.field]: {
|
||||||
|
$gt: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort({
|
||||||
|
[definition.field]: -1
|
||||||
|
})
|
||||||
|
.limit(limit)
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
const medals = [
|
||||||
|
"🥇",
|
||||||
|
"🥈",
|
||||||
|
"🥉"
|
||||||
|
];
|
||||||
|
|
||||||
|
const description =
|
||||||
|
rows.length > 0
|
||||||
|
? rows.map(
|
||||||
|
(row, index) => {
|
||||||
|
const value =
|
||||||
|
getNestedValue(
|
||||||
|
row,
|
||||||
|
definition.field
|
||||||
|
);
|
||||||
|
|
||||||
|
const rank =
|
||||||
|
medals[index] ??
|
||||||
|
`**${index + 1}.**`;
|
||||||
|
|
||||||
|
return `${rank} <@${row.userId}> — **${definition.format(value)}**`;
|
||||||
|
}
|
||||||
|
).join("\n")
|
||||||
|
: "No qualifying records yet.";
|
||||||
|
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle(
|
||||||
|
`📚 Records — ${definition.label}`
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
description
|
||||||
|
)
|
||||||
|
.setFooter({
|
||||||
|
text:
|
||||||
|
"Records are derived from persisted profile statistics."
|
||||||
|
})
|
||||||
|
.setTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecordChoices() {
|
||||||
|
return Object.entries(RECORDS)
|
||||||
|
.map(
|
||||||
|
([value, definition]) => ({
|
||||||
|
name:
|
||||||
|
definition.label,
|
||||||
|
value
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildRecordsEmbed,
|
||||||
|
getRecordChoices
|
||||||
|
};
|
||||||
+2083
-756
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user