Sends one-shot DMs once a mission is evaluable asking members to rate their leadership (and leaders to rate subordinates), gated by a new evaluationRemindersSentAt marker on Missions with its migration. Adds the /remind-evaluations staff command (manual trigger/sweep) and DISCORD_EVALUATION_POLL_MS config; updates bot and root docs. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
116 lines
4 KiB
TypeScript
116 lines
4 KiB
TypeScript
import { MessageFlags, SlashCommandBuilder, type Client } from "discord.js";
|
|
import type { Payload } from "payload";
|
|
import type { BotCommand } from "./index";
|
|
import { isStaff } from "@/bot/lib/roles";
|
|
import { processMission, reconcile } from "@/bot/services/evaluationReminders";
|
|
import { isMissionEvaluable } from "@/lib/evaluations";
|
|
import type { Mission } from "@/payload-types";
|
|
|
|
const missionLabel = (mission: Mission): string =>
|
|
mission.codeName ? `${mission.name} (${mission.codeName})` : mission.name;
|
|
|
|
const resolveTargetMissions = async (
|
|
payload: Payload,
|
|
input: string,
|
|
): Promise<{ mission?: Mission; error?: string }> => {
|
|
const trimmed = input.trim();
|
|
try {
|
|
const mission = await payload.findByID({
|
|
collection: "missions",
|
|
id: trimmed,
|
|
overrideAccess: true,
|
|
depth: 0,
|
|
});
|
|
return { mission: mission as unknown as Mission };
|
|
} catch {
|
|
// Not an ID — fall through to a name / code-name search.
|
|
}
|
|
|
|
const res = await payload.find({
|
|
collection: "missions",
|
|
overrideAccess: true,
|
|
depth: 0,
|
|
limit: 6,
|
|
where: {
|
|
or: [{ name: { like: trimmed } }, { codeName: { like: trimmed } }],
|
|
},
|
|
});
|
|
const docs = res.docs as unknown as Mission[];
|
|
if (docs.length === 0) {
|
|
return { error: `No mission matches "${trimmed}" (searched by ID, name, and code name).` };
|
|
}
|
|
if (docs.length > 1) {
|
|
const list = docs.map((m) => `• ${missionLabel(m)} — #${m.id}`).join("\n");
|
|
return { error: `Multiple missions match "${trimmed}". Be more specific:\n${list}` };
|
|
}
|
|
return { mission: docs[0] };
|
|
};
|
|
|
|
export const remindEvaluationsCommand: BotCommand = {
|
|
builder: new SlashCommandBuilder()
|
|
.setName("remind-evaluations")
|
|
.setDescription("Send leadership evaluation reminder DMs (staff only)")
|
|
.addStringOption((option) =>
|
|
option
|
|
.setName("mission")
|
|
.setDescription("Mission ID, name, or code name. Omit to sweep all pending missions."),
|
|
) as SlashCommandBuilder,
|
|
execute: async (interaction, payload) => {
|
|
if (!interaction.inGuild() || !interaction.guild) {
|
|
await interaction.reply({
|
|
content: "This command only works in a server.",
|
|
flags: MessageFlags.Ephemeral,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const member = await interaction.guild.members.fetch(interaction.user.id);
|
|
if (!(await isStaff(member, payload))) {
|
|
await interaction.reply({
|
|
content: "You don't have permission to trigger evaluation reminders.",
|
|
flags: MessageFlags.Ephemeral,
|
|
});
|
|
return;
|
|
}
|
|
|
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
|
|
const client = interaction.client as Client;
|
|
const missionInput = interaction.options.getString("mission")?.trim();
|
|
|
|
try {
|
|
if (missionInput) {
|
|
const { mission, error } = await resolveTargetMissions(payload, missionInput);
|
|
if (error || !mission) {
|
|
await interaction.editReply(error ?? `No mission matches "${missionInput}".`);
|
|
return;
|
|
}
|
|
if (
|
|
!isMissionEvaluable(
|
|
mission.ownershipAndStatus?.status ?? "",
|
|
mission.classification?.startDateTime ?? null,
|
|
)
|
|
) {
|
|
await interaction.editReply(
|
|
`**${missionLabel(mission)}** isn't evaluable yet — missions must be completed or past their scheduled start before reminders go out.`,
|
|
);
|
|
return;
|
|
}
|
|
const sent = await processMission(client, payload, mission);
|
|
await interaction.editReply(
|
|
`Done — ${sent} evaluation reminder DM(s) sent for **${missionLabel(mission)}**.`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const { processed, sent } = await reconcile(client, payload);
|
|
await interaction.editReply(
|
|
`Sweep complete — ${processed} pending mission(s) processed, ${sent} reminder DM(s) sent.`,
|
|
);
|
|
} catch (error) {
|
|
await interaction.editReply(
|
|
`Reminder trigger failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
);
|
|
}
|
|
},
|
|
};
|