630 lines
20 KiB
JavaScript
630 lines
20 KiB
JavaScript
const {
|
|
SlashCommandBuilder,
|
|
MessageFlags,
|
|
EmbedBuilder
|
|
} = require("discord.js");
|
|
|
|
const {
|
|
reconcileUserAchievements,
|
|
reconcileAllAchievements,
|
|
grantMaintenanceReward,
|
|
getMaintenanceRewardStatus,
|
|
auditProfileData,
|
|
DEFAULT_MAINTENANCE_REWARD_ID,
|
|
DEFAULT_MAINTENANCE_REWARD_AMOUNT
|
|
} = require(
|
|
"../../utils/profileMaintenance"
|
|
);
|
|
|
|
const {
|
|
requireAdminAccess
|
|
} = require(
|
|
"../../utils/permissions"
|
|
);
|
|
|
|
|
|
module.exports = {
|
|
data:
|
|
new SlashCommandBuilder()
|
|
.setName(
|
|
"profileadmin"
|
|
)
|
|
.setDescription(
|
|
"Administrative profile maintenance tools"
|
|
)
|
|
.addSubcommand(
|
|
subcommand =>
|
|
subcommand
|
|
.setName(
|
|
"reconcile"
|
|
)
|
|
.setDescription(
|
|
"Backfill database-trackable achievements"
|
|
)
|
|
.addUserOption(
|
|
option =>
|
|
option
|
|
.setName(
|
|
"user"
|
|
)
|
|
.setDescription(
|
|
"Only reconcile this user; omit for everyone"
|
|
)
|
|
)
|
|
.addBooleanOption(
|
|
option =>
|
|
option
|
|
.setName(
|
|
"dry-run"
|
|
)
|
|
.setDescription(
|
|
"Show what would change without writing it"
|
|
)
|
|
)
|
|
)
|
|
.addSubcommand(
|
|
subcommand =>
|
|
subcommand
|
|
.setName(
|
|
"audit"
|
|
)
|
|
.setDescription(
|
|
"Audit stored profile statistics for inconsistencies"
|
|
)
|
|
)
|
|
.addSubcommand(
|
|
subcommand =>
|
|
subcommand
|
|
.setName(
|
|
"maintenance-reward"
|
|
)
|
|
.setDescription(
|
|
"Grant an idempotent maintenance reward to stored profile users"
|
|
)
|
|
.addStringOption(
|
|
option =>
|
|
option
|
|
.setName(
|
|
"reward-id"
|
|
)
|
|
.setDescription(
|
|
`Unique reward ID; defaults to ${DEFAULT_MAINTENANCE_REWARD_ID}`
|
|
)
|
|
)
|
|
.addIntegerOption(
|
|
option =>
|
|
option
|
|
.setName(
|
|
"amount"
|
|
)
|
|
.setDescription(
|
|
`Cookies per player; defaults to ${DEFAULT_MAINTENANCE_REWARD_AMOUNT}`
|
|
)
|
|
.setMinValue(
|
|
1
|
|
)
|
|
)
|
|
.addBooleanOption(
|
|
option =>
|
|
option
|
|
.setName(
|
|
"dry-run"
|
|
)
|
|
.setDescription(
|
|
"Preview recipients without granting Cookies"
|
|
)
|
|
)
|
|
)
|
|
.addSubcommand(
|
|
subcommand =>
|
|
subcommand
|
|
.setName(
|
|
"maintenance-status"
|
|
)
|
|
.setDescription(
|
|
"Show maintenance reward claim status"
|
|
)
|
|
.addStringOption(
|
|
option =>
|
|
option
|
|
.setName(
|
|
"reward-id"
|
|
)
|
|
.setDescription(
|
|
`Reward ID; defaults to ${DEFAULT_MAINTENANCE_REWARD_ID}`
|
|
)
|
|
)
|
|
),
|
|
|
|
async execute(
|
|
interaction
|
|
) {
|
|
/*
|
|
* Allows:
|
|
*
|
|
* - Discord Administrators
|
|
* - BOT_AUTHOR_ID from .env
|
|
*/
|
|
|
|
if (
|
|
!await requireAdminAccess(
|
|
interaction
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
|
|
const subcommand =
|
|
interaction.options.getSubcommand();
|
|
|
|
|
|
await interaction.deferReply({
|
|
flags:
|
|
MessageFlags.Ephemeral
|
|
});
|
|
|
|
|
|
try {
|
|
if (
|
|
subcommand ===
|
|
"reconcile"
|
|
) {
|
|
const user =
|
|
interaction.options.getUser(
|
|
"user"
|
|
);
|
|
|
|
|
|
const dryRun =
|
|
interaction.options.getBoolean(
|
|
"dry-run"
|
|
) ?? false;
|
|
|
|
|
|
if (user) {
|
|
const result =
|
|
await reconcileUserAchievements({
|
|
guildId:
|
|
interaction.guildId,
|
|
|
|
userId:
|
|
user.id,
|
|
|
|
dryRun
|
|
});
|
|
|
|
|
|
return interaction.editReply({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🛠️ Profile Reconciliation"
|
|
)
|
|
.setDescription(
|
|
`${dryRun ? "Dry run for" : "Reconciled"} ${user}.`
|
|
)
|
|
.addFields(
|
|
{
|
|
name:
|
|
"New unlocks",
|
|
|
|
value:
|
|
`**${result.unlocked}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"Definitions checked",
|
|
|
|
value:
|
|
`**${result.checked}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"Event-only skipped",
|
|
|
|
value:
|
|
`**${result.skippedEventOnly}**`,
|
|
|
|
inline:
|
|
true
|
|
}
|
|
)
|
|
]
|
|
});
|
|
}
|
|
|
|
|
|
const summary =
|
|
await reconcileAllAchievements({
|
|
guildId:
|
|
interaction.guildId,
|
|
|
|
dryRun
|
|
});
|
|
|
|
|
|
return interaction.editReply({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🛠️ Profile Reconciliation"
|
|
)
|
|
.setDescription(
|
|
`${dryRun ? "Dry run completed" : "Reconciliation completed"} for stored profile users.`
|
|
)
|
|
.addFields(
|
|
{
|
|
name:
|
|
"Users",
|
|
|
|
value:
|
|
`**${summary.users}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"New unlocks",
|
|
|
|
value:
|
|
`**${summary.unlocked}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"Event-only checks skipped",
|
|
|
|
value:
|
|
`**${summary.skippedEventOnly}**`,
|
|
|
|
inline:
|
|
true
|
|
}
|
|
)
|
|
]
|
|
});
|
|
}
|
|
|
|
|
|
if (
|
|
subcommand ===
|
|
"maintenance-reward"
|
|
) {
|
|
const rewardId =
|
|
interaction.options.getString(
|
|
"reward-id"
|
|
)
|
|
??
|
|
DEFAULT_MAINTENANCE_REWARD_ID;
|
|
|
|
|
|
const amount =
|
|
interaction.options.getInteger(
|
|
"amount"
|
|
)
|
|
??
|
|
DEFAULT_MAINTENANCE_REWARD_AMOUNT;
|
|
|
|
|
|
/*
|
|
* Safer admin default:
|
|
*
|
|
* If omitted, maintenance-reward previews
|
|
* instead of paying immediately.
|
|
*
|
|
* Pass dry-run:false explicitly to execute.
|
|
*/
|
|
|
|
const dryRun =
|
|
interaction.options.getBoolean(
|
|
"dry-run"
|
|
)
|
|
??
|
|
true;
|
|
|
|
|
|
const summary =
|
|
await grantMaintenanceReward({
|
|
guildId:
|
|
interaction.guildId,
|
|
|
|
rewardId,
|
|
|
|
amount,
|
|
|
|
dryRun
|
|
});
|
|
|
|
|
|
return interaction.editReply({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
dryRun
|
|
? "🧪 Maintenance Reward Dry Run"
|
|
: "🎁 Maintenance Reward"
|
|
)
|
|
.setDescription(
|
|
dryRun
|
|
? "No Cookies were granted. Run again with `dry-run:false` to execute this reward."
|
|
: `Maintenance compensation **${rewardId}** has been processed.`
|
|
)
|
|
.addFields(
|
|
{
|
|
name:
|
|
"Reward ID",
|
|
|
|
value:
|
|
`\`${summary.rewardId}\``,
|
|
|
|
inline:
|
|
false
|
|
},
|
|
{
|
|
name:
|
|
"Per player",
|
|
|
|
value:
|
|
`**${summary.amount.toLocaleString()} 🍪**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"Stored profiles",
|
|
|
|
value:
|
|
`**${summary.users}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
dryRun
|
|
? "Would receive"
|
|
: "Granted",
|
|
|
|
value:
|
|
`**${dryRun ? summary.eligible : summary.granted}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"Already claimed",
|
|
|
|
value:
|
|
`**${summary.alreadyClaimed}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"Failed",
|
|
|
|
value:
|
|
`**${summary.failed}**`,
|
|
|
|
inline:
|
|
true
|
|
}
|
|
)
|
|
]
|
|
});
|
|
}
|
|
|
|
|
|
if (
|
|
subcommand ===
|
|
"maintenance-status"
|
|
) {
|
|
const rewardId =
|
|
interaction.options.getString(
|
|
"reward-id"
|
|
)
|
|
??
|
|
DEFAULT_MAINTENANCE_REWARD_ID;
|
|
|
|
|
|
const status =
|
|
await getMaintenanceRewardStatus({
|
|
guildId:
|
|
interaction.guildId,
|
|
|
|
rewardId
|
|
});
|
|
|
|
|
|
return interaction.editReply({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🎁 Maintenance Reward Status"
|
|
)
|
|
.addFields(
|
|
{
|
|
name:
|
|
"Reward ID",
|
|
|
|
value:
|
|
`\`${status.rewardId}\``,
|
|
|
|
inline:
|
|
false
|
|
},
|
|
{
|
|
name:
|
|
"Default amount",
|
|
|
|
value:
|
|
`**${status.amount.toLocaleString()} 🍪**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"Stored profiles",
|
|
|
|
value:
|
|
`**${status.users}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"Claimed",
|
|
|
|
value:
|
|
`**${status.claimed}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"Remaining",
|
|
|
|
value:
|
|
`**${status.remaining}**`,
|
|
|
|
inline:
|
|
true
|
|
}
|
|
)
|
|
]
|
|
});
|
|
}
|
|
|
|
|
|
const audit =
|
|
await auditProfileData(
|
|
interaction.guildId
|
|
);
|
|
|
|
|
|
const issuePreview =
|
|
audit.issues
|
|
.slice(
|
|
0,
|
|
10
|
|
)
|
|
.join(
|
|
"\n"
|
|
);
|
|
|
|
|
|
return interaction.editReply({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"🔎 Profile Data Audit"
|
|
)
|
|
.addFields(
|
|
{
|
|
name:
|
|
"Stats documents",
|
|
|
|
value:
|
|
`**${audit.statDocuments}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"Achievement documents",
|
|
|
|
value:
|
|
`**${audit.achievementDocuments}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"Active achievements",
|
|
|
|
value:
|
|
`**${audit.activeAchievements}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"Global badges",
|
|
|
|
value:
|
|
`**${audit.badgeDocuments}** (${audit.claimedBadges} claimed)`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"Orphan achievement docs",
|
|
|
|
value:
|
|
`**${audit.orphanAchievementDocs}**`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
{
|
|
name:
|
|
"Issues",
|
|
|
|
value:
|
|
`**${audit.issueCount}**`,
|
|
|
|
inline:
|
|
true
|
|
}
|
|
)
|
|
.setDescription(
|
|
issuePreview
|
|
? `First issues:\n${issuePreview}`
|
|
: "No structural inconsistencies found."
|
|
)
|
|
]
|
|
});
|
|
}
|
|
|
|
catch (error) {
|
|
console.error(
|
|
`[ProfileAdmin] ${subcommand} failed:`,
|
|
error
|
|
);
|
|
|
|
|
|
return interaction.editReply({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle(
|
|
"❌ Profile Admin Error"
|
|
)
|
|
.setDescription(
|
|
"The maintenance action failed. Check the bot logs for details."
|
|
)
|
|
]
|
|
});
|
|
}
|
|
}
|
|
};
|