1
0
Fork 0
polaris-task-force/src/bot/config.ts
Z8MB1E efae94c8a9 feat(bot): add Friday open-slot notice with claim credit and revert
At 8 PM Eastern on Wednesday, if no non-cancelled mission starts on that
week's upcoming Friday, post once in #side-ops pinging the Community
Zeus role (DISCORD_COMMUNITY_ZEUS_ROLE_ID, or the role resolved by
name): the slot is open, hosting is welcome, and otherwise the slot
stays empty or Jason hosts it himself. Dedup uses a stable per-Friday
timestamp token scanned from recent channel messages, so no DB state.
When a non-superuser later claims the slot the notice is edited to
credit them; a cancelled or deleted claim reverts the notice, and a
between-polls claim swap re-credits the new claimant.
2026-09-18 14:52:39 -04:00

61 lines
2 KiB
TypeScript

export interface BotConfig {
token: string;
guildId: string;
appUrl: string;
opsChannelId: string | null;
announceChannelId: string | null;
staffRoleIds: string[];
sideOpsChannelId: string | null;
communityZeusRoleId: string | null;
attendancePollMs: number;
notificationPollMs: number;
evaluationPollMs: number;
readyReminderPollMs: number;
imminentReminderPollMs: number;
sideOpsNoticePollMs: number;
}
const required = (name: string): string => {
const value = process.env[name]?.trim();
if (!value) {
throw new Error(`[Bot] Missing required environment variable: ${name}`);
}
return value;
};
const optional = (name: string): string | null => {
const value = process.env[name]?.trim();
return value ? value : null;
};
const pollMs = (name: string, fallback: number): number => {
const raw = process.env[name]?.trim();
if (!raw) {
return fallback;
}
const value = Number.parseInt(raw, 10);
if (!Number.isInteger(value) || value <= 0) {
throw new Error(`[Bot] Invalid ${name}: expected a positive integer, got "${raw}"`);
}
return value;
};
export const botConfig: BotConfig = {
token: required("DISCORD_TOKEN"),
guildId: required("DISCORD_GUILD_ID"),
appUrl: required("APP_URL"),
opsChannelId: optional("DISCORD_OPS_CHANNEL_ID"),
announceChannelId: optional("DISCORD_ANNOUNCE_CHANNEL_ID"),
staffRoleIds: (optional("DISCORD_STAFF_ROLE_IDS") ?? "")
.split(",")
.map((id) => id.trim())
.filter((id) => id.length > 0),
sideOpsChannelId: optional("DISCORD_SIDE_OPS_CHANNEL_ID"),
communityZeusRoleId: optional("DISCORD_COMMUNITY_ZEUS_ROLE_ID"),
attendancePollMs: pollMs("DISCORD_ATTENDANCE_POLL_MS", 60_000),
notificationPollMs: pollMs("DISCORD_NOTIFICATION_POLL_MS", 20_000),
evaluationPollMs: pollMs("DISCORD_EVALUATION_POLL_MS", 60_000),
readyReminderPollMs: pollMs("DISCORD_READY_REMINDER_POLL_MS", 60_000),
imminentReminderPollMs: pollMs("DISCORD_IMMINENT_POLL_MS", 60_000),
sideOpsNoticePollMs: pollMs("DISCORD_SIDE_OPS_POLL_MS", 1_800_000),
};