415 lines
9.5 KiB
JavaScript
415 lines
9.5 KiB
JavaScript
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."
|
|
});
|
|
}
|
|
} |