updated profile and centralized functions
This commit is contained in:
@@ -1,54 +1,51 @@
|
||||
const {
|
||||
requireProfileOwner,
|
||||
buildAchievementsEmbed,
|
||||
buildProfileButtons,
|
||||
getProfileContextFromFooter,
|
||||
requireProfileOwner
|
||||
} = require(
|
||||
"../../utils/profileSystem"
|
||||
);
|
||||
buildProfilePaginationButtons
|
||||
} = require("../../utils/profileSystem");
|
||||
|
||||
module.exports = {
|
||||
name:
|
||||
"profileAchievements",
|
||||
name: "profileAchievements",
|
||||
|
||||
async execute(
|
||||
interaction
|
||||
) {
|
||||
const context =
|
||||
await requireProfileOwner(
|
||||
async execute(interaction) {
|
||||
const session = await requireProfileOwner(
|
||||
interaction
|
||||
);
|
||||
|
||||
if (!context) {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user =
|
||||
await interaction.client.users.fetch(
|
||||
context.profileUserId
|
||||
const user = await interaction.client.users.fetch(
|
||||
session.profileUserId
|
||||
);
|
||||
|
||||
const embed =
|
||||
await buildAchievementsEmbed(
|
||||
const result = await buildAchievementsEmbed(
|
||||
interaction.guildId,
|
||||
user,
|
||||
context.sessionOwnerId,
|
||||
0
|
||||
0,
|
||||
session.sessionOwnerId
|
||||
);
|
||||
|
||||
const pageContext =
|
||||
getProfileContextFromFooter(
|
||||
embed.data.footer?.text
|
||||
const components = [
|
||||
...buildProfileButtons()
|
||||
];
|
||||
|
||||
if (result.pageCount > 1) {
|
||||
components.push(
|
||||
buildProfilePaginationButtons(
|
||||
"profileAchievementsLeft",
|
||||
"profileAchievementsRight",
|
||||
result.page,
|
||||
result.pageCount
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return interaction.update({
|
||||
embeds: [embed],
|
||||
components:
|
||||
buildProfileButtons(
|
||||
"achievements",
|
||||
pageContext?.page ?? 0,
|
||||
pageContext?.pageCount ?? 1
|
||||
)
|
||||
embeds: [result.embed],
|
||||
components
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,54 +1,53 @@
|
||||
// buttons/profile/achievementsLeft.js
|
||||
|
||||
const {
|
||||
requireProfileOwner,
|
||||
buildAchievementsEmbed,
|
||||
buildProfileButtons,
|
||||
getProfileContextFromFooter,
|
||||
requireProfileOwner
|
||||
} = require(
|
||||
"../../utils/profileSystem"
|
||||
);
|
||||
buildProfilePaginationButtons
|
||||
} = require("../../utils/profileSystem");
|
||||
|
||||
module.exports = {
|
||||
name:
|
||||
"profileAchievementsLeft",
|
||||
name: "profileAchievementsLeft",
|
||||
|
||||
async execute(
|
||||
interaction
|
||||
) {
|
||||
const context =
|
||||
await requireProfileOwner(
|
||||
async execute(interaction) {
|
||||
const session = await requireProfileOwner(
|
||||
interaction
|
||||
);
|
||||
|
||||
if (!context) {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user =
|
||||
await interaction.client.users.fetch(
|
||||
context.profileUserId
|
||||
const user = await interaction.client.users.fetch(
|
||||
session.profileUserId
|
||||
);
|
||||
|
||||
const embed =
|
||||
await buildAchievementsEmbed(
|
||||
const result = await buildAchievementsEmbed(
|
||||
interaction.guildId,
|
||||
user,
|
||||
context.sessionOwnerId,
|
||||
context.page - 1
|
||||
session.page - 1,
|
||||
session.sessionOwnerId
|
||||
);
|
||||
|
||||
const pageContext =
|
||||
getProfileContextFromFooter(
|
||||
embed.data.footer?.text
|
||||
const components = [
|
||||
...buildProfileButtons()
|
||||
];
|
||||
|
||||
if (result.pageCount > 1) {
|
||||
components.push(
|
||||
buildProfilePaginationButtons(
|
||||
"profileAchievementsLeft",
|
||||
"profileAchievementsRight",
|
||||
result.page,
|
||||
result.pageCount
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return interaction.update({
|
||||
embeds: [embed],
|
||||
components:
|
||||
buildProfileButtons(
|
||||
"achievements",
|
||||
pageContext?.page ?? 0,
|
||||
pageContext?.pageCount ?? 1
|
||||
)
|
||||
embeds: [result.embed],
|
||||
components
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,54 +1,53 @@
|
||||
// buttons/profile/achievementsRight.js
|
||||
|
||||
const {
|
||||
requireProfileOwner,
|
||||
buildAchievementsEmbed,
|
||||
buildProfileButtons,
|
||||
getProfileContextFromFooter,
|
||||
requireProfileOwner
|
||||
} = require(
|
||||
"../../utils/profileSystem"
|
||||
);
|
||||
buildProfilePaginationButtons
|
||||
} = require("../../utils/profileSystem");
|
||||
|
||||
module.exports = {
|
||||
name:
|
||||
"profileAchievementsRight",
|
||||
name: "profileAchievementsRight",
|
||||
|
||||
async execute(
|
||||
interaction
|
||||
) {
|
||||
const context =
|
||||
await requireProfileOwner(
|
||||
async execute(interaction) {
|
||||
const session = await requireProfileOwner(
|
||||
interaction
|
||||
);
|
||||
|
||||
if (!context) {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user =
|
||||
await interaction.client.users.fetch(
|
||||
context.profileUserId
|
||||
const user = await interaction.client.users.fetch(
|
||||
session.profileUserId
|
||||
);
|
||||
|
||||
const embed =
|
||||
await buildAchievementsEmbed(
|
||||
const result = await buildAchievementsEmbed(
|
||||
interaction.guildId,
|
||||
user,
|
||||
context.sessionOwnerId,
|
||||
context.page + 1
|
||||
session.page + 1,
|
||||
session.sessionOwnerId
|
||||
);
|
||||
|
||||
const pageContext =
|
||||
getProfileContextFromFooter(
|
||||
embed.data.footer?.text
|
||||
const components = [
|
||||
...buildProfileButtons()
|
||||
];
|
||||
|
||||
if (result.pageCount > 1) {
|
||||
components.push(
|
||||
buildProfilePaginationButtons(
|
||||
"profileAchievementsLeft",
|
||||
"profileAchievementsRight",
|
||||
result.page,
|
||||
result.pageCount
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return interaction.update({
|
||||
embeds: [embed],
|
||||
components:
|
||||
buildProfileButtons(
|
||||
"achievements",
|
||||
pageContext?.page ?? 0,
|
||||
pageContext?.pageCount ?? 1
|
||||
)
|
||||
embeds: [result.embed],
|
||||
components
|
||||
});
|
||||
}
|
||||
};
|
||||
+27
-30
@@ -1,54 +1,51 @@
|
||||
const {
|
||||
requireProfileOwner,
|
||||
buildBadgesEmbed,
|
||||
buildProfileButtons,
|
||||
getProfileContextFromFooter,
|
||||
requireProfileOwner
|
||||
} = require(
|
||||
"../../utils/profileSystem"
|
||||
);
|
||||
buildProfilePaginationButtons
|
||||
} = require("../../utils/profileSystem");
|
||||
|
||||
module.exports = {
|
||||
name:
|
||||
"profileBadges",
|
||||
name: "profileBadges",
|
||||
|
||||
async execute(
|
||||
interaction
|
||||
) {
|
||||
const context =
|
||||
await requireProfileOwner(
|
||||
async execute(interaction) {
|
||||
const session = await requireProfileOwner(
|
||||
interaction
|
||||
);
|
||||
|
||||
if (!context) {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user =
|
||||
await interaction.client.users.fetch(
|
||||
context.profileUserId
|
||||
const user = await interaction.client.users.fetch(
|
||||
session.profileUserId
|
||||
);
|
||||
|
||||
const embed =
|
||||
await buildBadgesEmbed(
|
||||
const result = await buildBadgesEmbed(
|
||||
interaction.guildId,
|
||||
user,
|
||||
context.sessionOwnerId,
|
||||
0
|
||||
0,
|
||||
session.sessionOwnerId
|
||||
);
|
||||
|
||||
const pageContext =
|
||||
getProfileContextFromFooter(
|
||||
embed.data.footer?.text
|
||||
const components = [
|
||||
...buildProfileButtons()
|
||||
];
|
||||
|
||||
if (result.pageCount > 1) {
|
||||
components.push(
|
||||
buildProfilePaginationButtons(
|
||||
"profileBadgesLeft",
|
||||
"profileBadgesRight",
|
||||
result.page,
|
||||
result.pageCount
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return interaction.update({
|
||||
embeds: [embed],
|
||||
components:
|
||||
buildProfileButtons(
|
||||
"badges",
|
||||
pageContext?.page ?? 0,
|
||||
pageContext?.pageCount ?? 1
|
||||
)
|
||||
embeds: [result.embed],
|
||||
components
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,54 +1,53 @@
|
||||
// buttons/profile/badgesLeft.js
|
||||
|
||||
const {
|
||||
requireProfileOwner,
|
||||
buildBadgesEmbed,
|
||||
buildProfileButtons,
|
||||
getProfileContextFromFooter,
|
||||
requireProfileOwner
|
||||
} = require(
|
||||
"../../utils/profileSystem"
|
||||
);
|
||||
buildProfilePaginationButtons
|
||||
} = require("../../utils/profileSystem");
|
||||
|
||||
module.exports = {
|
||||
name:
|
||||
"profileBadgesLeft",
|
||||
name: "profileBadgesLeft",
|
||||
|
||||
async execute(
|
||||
interaction
|
||||
) {
|
||||
const context =
|
||||
await requireProfileOwner(
|
||||
async execute(interaction) {
|
||||
const session = await requireProfileOwner(
|
||||
interaction
|
||||
);
|
||||
|
||||
if (!context) {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user =
|
||||
await interaction.client.users.fetch(
|
||||
context.profileUserId
|
||||
const user = await interaction.client.users.fetch(
|
||||
session.profileUserId
|
||||
);
|
||||
|
||||
const embed =
|
||||
await buildBadgesEmbed(
|
||||
const result = await buildBadgesEmbed(
|
||||
interaction.guildId,
|
||||
user,
|
||||
context.sessionOwnerId,
|
||||
context.page - 1
|
||||
session.page - 1,
|
||||
session.sessionOwnerId
|
||||
);
|
||||
|
||||
const pageContext =
|
||||
getProfileContextFromFooter(
|
||||
embed.data.footer?.text
|
||||
const components = [
|
||||
...buildProfileButtons()
|
||||
];
|
||||
|
||||
if (result.pageCount > 1) {
|
||||
components.push(
|
||||
buildProfilePaginationButtons(
|
||||
"profileBadgesLeft",
|
||||
"profileBadgesRight",
|
||||
result.page,
|
||||
result.pageCount
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return interaction.update({
|
||||
embeds: [embed],
|
||||
components:
|
||||
buildProfileButtons(
|
||||
"badges",
|
||||
pageContext?.page ?? 0,
|
||||
pageContext?.pageCount ?? 1
|
||||
)
|
||||
embeds: [result.embed],
|
||||
components
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,54 +1,53 @@
|
||||
// buttons/profile/badgesRight.js
|
||||
|
||||
const {
|
||||
requireProfileOwner,
|
||||
buildBadgesEmbed,
|
||||
buildProfileButtons,
|
||||
getProfileContextFromFooter,
|
||||
requireProfileOwner
|
||||
} = require(
|
||||
"../../utils/profileSystem"
|
||||
);
|
||||
buildProfilePaginationButtons
|
||||
} = require("../../utils/profileSystem");
|
||||
|
||||
module.exports = {
|
||||
name:
|
||||
"profileBadgesRight",
|
||||
name: "profileBadgesRight",
|
||||
|
||||
async execute(
|
||||
interaction
|
||||
) {
|
||||
const context =
|
||||
await requireProfileOwner(
|
||||
async execute(interaction) {
|
||||
const session = await requireProfileOwner(
|
||||
interaction
|
||||
);
|
||||
|
||||
if (!context) {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user =
|
||||
await interaction.client.users.fetch(
|
||||
context.profileUserId
|
||||
const user = await interaction.client.users.fetch(
|
||||
session.profileUserId
|
||||
);
|
||||
|
||||
const embed =
|
||||
await buildBadgesEmbed(
|
||||
const result = await buildBadgesEmbed(
|
||||
interaction.guildId,
|
||||
user,
|
||||
context.sessionOwnerId,
|
||||
context.page + 1
|
||||
session.page + 1,
|
||||
session.sessionOwnerId
|
||||
);
|
||||
|
||||
const pageContext =
|
||||
getProfileContextFromFooter(
|
||||
embed.data.footer?.text
|
||||
const components = [
|
||||
...buildProfileButtons()
|
||||
];
|
||||
|
||||
if (result.pageCount > 1) {
|
||||
components.push(
|
||||
buildProfilePaginationButtons(
|
||||
"profileBadgesLeft",
|
||||
"profileBadgesRight",
|
||||
result.page,
|
||||
result.pageCount
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return interaction.update({
|
||||
embeds: [embed],
|
||||
components:
|
||||
buildProfileButtons(
|
||||
"badges",
|
||||
pageContext?.page ?? 0,
|
||||
pageContext?.pageCount ?? 1
|
||||
)
|
||||
embeds: [result.embed],
|
||||
components
|
||||
});
|
||||
}
|
||||
};
|
||||
+13
-26
@@ -1,47 +1,34 @@
|
||||
const {
|
||||
buildStatsEmbed,
|
||||
buildProfileButtons,
|
||||
requireProfileOwner
|
||||
} = require(
|
||||
"../../utils/profileSystem"
|
||||
);
|
||||
requireProfileOwner,
|
||||
buildProfileStatsEmbed,
|
||||
buildProfileButtons
|
||||
} = require("../../utils/profileSystem");
|
||||
|
||||
module.exports = {
|
||||
name:
|
||||
"profileStats",
|
||||
name: "profileStats",
|
||||
|
||||
async execute(
|
||||
interaction
|
||||
) {
|
||||
const context =
|
||||
await requireProfileOwner(
|
||||
async execute(interaction) {
|
||||
const session = await requireProfileOwner(
|
||||
interaction
|
||||
);
|
||||
|
||||
if (!context) {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user =
|
||||
await interaction.client.users.fetch(
|
||||
context.profileUserId
|
||||
const user = await interaction.client.users.fetch(
|
||||
session.profileUserId
|
||||
);
|
||||
|
||||
const embed =
|
||||
await buildStatsEmbed(
|
||||
const embed = await buildProfileStatsEmbed(
|
||||
interaction.guildId,
|
||||
user,
|
||||
context.sessionOwnerId
|
||||
session.sessionOwnerId
|
||||
);
|
||||
|
||||
return interaction.update({
|
||||
embeds: [embed],
|
||||
components:
|
||||
buildProfileButtons(
|
||||
"stats",
|
||||
0,
|
||||
1
|
||||
)
|
||||
components: buildProfileButtons()
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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]
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -53,7 +53,6 @@ const client =
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
exports.client =
|
||||
client;
|
||||
|
||||
@@ -87,7 +86,6 @@ client.modals =
|
||||
|
||||
async function start() {
|
||||
try {
|
||||
|
||||
/*
|
||||
* Connect Mongo first.
|
||||
*/
|
||||
@@ -99,6 +97,37 @@ async function start() {
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* 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 {
|
||||
ensureCookieIndexes
|
||||
} = require(
|
||||
"./utils/cookieEconomy"
|
||||
);
|
||||
|
||||
const {
|
||||
ensureProfileSystem
|
||||
} = require(
|
||||
"./utils/profileSystem"
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
ensureCookieIndexes(),
|
||||
ensureProfileSystem()
|
||||
]);
|
||||
|
||||
console.log(
|
||||
"Database indexes initialized"
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Load handlers.
|
||||
*/
|
||||
@@ -108,7 +137,6 @@ async function start() {
|
||||
"./handlers"
|
||||
);
|
||||
|
||||
|
||||
for (
|
||||
const handler
|
||||
of handlerFiles
|
||||
@@ -137,5 +165,4 @@ async function start() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
start();
|
||||
+4
-26
@@ -15,7 +15,8 @@ const {
|
||||
} = require("./cookieEconomy");
|
||||
|
||||
const {
|
||||
trackProfileEvent
|
||||
trackProfileEvent,
|
||||
recordGameOutcome
|
||||
} = require("./profileSystem");
|
||||
|
||||
const HEIST_COOLDOWN =
|
||||
@@ -255,12 +256,10 @@ async function resolveCookieHeist(
|
||||
throw error;
|
||||
}
|
||||
|
||||
await trackProfileEvent(
|
||||
await recordGameOutcome(
|
||||
interaction,
|
||||
playerId,
|
||||
{
|
||||
type:
|
||||
"game_result",
|
||||
game:
|
||||
"heist",
|
||||
result:
|
||||
@@ -280,15 +279,6 @@ async function resolveCookieHeist(
|
||||
}
|
||||
);
|
||||
|
||||
await trackProfileEvent(
|
||||
interaction,
|
||||
playerId,
|
||||
{
|
||||
type:
|
||||
"cookie_gain",
|
||||
amount
|
||||
}
|
||||
);
|
||||
|
||||
const [
|
||||
playerAfter,
|
||||
@@ -444,12 +434,10 @@ async function resolveCookieHeist(
|
||||
throw error;
|
||||
}
|
||||
|
||||
await trackProfileEvent(
|
||||
await recordGameOutcome(
|
||||
interaction,
|
||||
playerId,
|
||||
{
|
||||
type:
|
||||
"game_result",
|
||||
game:
|
||||
"heist",
|
||||
result:
|
||||
@@ -459,16 +447,6 @@ async function resolveCookieHeist(
|
||||
}
|
||||
);
|
||||
|
||||
await trackProfileEvent(
|
||||
interaction,
|
||||
playerId,
|
||||
{
|
||||
type:
|
||||
"cookie_loss",
|
||||
amount:
|
||||
penalty
|
||||
}
|
||||
);
|
||||
|
||||
const [
|
||||
playerAfter,
|
||||
|
||||
+2
-30
@@ -18,7 +18,7 @@ const {
|
||||
} = require("./cookieEconomy");
|
||||
|
||||
const {
|
||||
trackProfileEvent
|
||||
recordGameOutcome
|
||||
} = require("./profileSystem");
|
||||
|
||||
const GAME_COLLECTION =
|
||||
@@ -929,12 +929,10 @@ async function finishBlackjack(
|
||||
profileResult = "loss";
|
||||
}
|
||||
|
||||
await trackProfileEvent(
|
||||
await recordGameOutcome(
|
||||
interaction,
|
||||
game.userId,
|
||||
{
|
||||
type:
|
||||
"game_result",
|
||||
game:
|
||||
"blackjack",
|
||||
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 = [
|
||||
"**Dealer**",
|
||||
formatCards(
|
||||
|
||||
+2
-30
@@ -12,7 +12,7 @@ const {
|
||||
} = require("./cookieEconomy");
|
||||
|
||||
const {
|
||||
trackProfileEvent
|
||||
recordGameOutcome
|
||||
} = require("./profileSystem");
|
||||
|
||||
const ROULETTE_BETS = [
|
||||
@@ -577,12 +577,10 @@ async function playRoulette(
|
||||
const profit =
|
||||
payout - bet;
|
||||
|
||||
await trackProfileEvent(
|
||||
await recordGameOutcome(
|
||||
interaction,
|
||||
playerId,
|
||||
{
|
||||
type:
|
||||
"game_result",
|
||||
game:
|
||||
"roulette",
|
||||
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 =
|
||||
new EmbedBuilder()
|
||||
.setTitle(
|
||||
|
||||
+2
-30
@@ -13,7 +13,7 @@ const {
|
||||
} = require("./cookieEconomy");
|
||||
|
||||
const {
|
||||
trackProfileEvent
|
||||
recordGameOutcome
|
||||
} = require("./profileSystem");
|
||||
|
||||
const SLOT_BETS = [
|
||||
@@ -308,12 +308,10 @@ async function playSlots(
|
||||
const profit =
|
||||
payout - bet;
|
||||
|
||||
await trackProfileEvent(
|
||||
await recordGameOutcome(
|
||||
interaction,
|
||||
playerId,
|
||||
{
|
||||
type:
|
||||
"game_result",
|
||||
game:
|
||||
"slots",
|
||||
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 =
|
||||
new EmbedBuilder()
|
||||
.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
|
||||
};
|
||||
+2038
-711
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user