1
0
Fork 0
polaris-task-force/src/bot/AGENTS.md

7.5 KiB

AGENTS.md — Discord Bot

Parent: ../../AGENTS.md — env vars (DISCORD_TOKEN, DISCORD_GUILD_ID), deployment, Payload config.

Overview

Standalone long-running process (bun run bot). Imports @payload-config directly, shares PostgreSQL with web app. Under active development.

Structure

bot/
  index.ts              # Entry point
  config.ts             # Env validation (fail-fast on missing required vars)

  commands/
    index.ts            # Command registry (global vs guild scope)
    ping.ts             # /ping (global, DM-usable)
    signup.ts           # /signup (DM-only, creates Payload user with temp password)
    link.ts             # /link (global, links discordId to existing user)
    announce.ts         # /announce (guild-only, staff only; message omitted = compose modal with channel picker, handler in this file)
    remindEvaluations.ts # /remind-evaluations (guild-only, staff only, manual evaluation-reminder trigger)

  events/
    interactionCreate.ts # Routes ptf-att: RSVP button interactions
    transferInteractions.ts # Handles transfer decision buttons (approve/deny/appeal)

  services/
    index.ts            # Service registry
    missionEmbeds.ts    # Attendance embed lifecycle + reconcile loop (poll tick)
    notificationBridge.ts # Poll user-notifications → Discord DMs
    evaluationReminders.ts # Post-mission evaluation reminder DMs (one-shot marker on Missions)
    imminentReminders.ts # Pre-op reminder: thread on the roll-call message + attendee pings (one-shot marker on Missions)
    fridayOpsNotice.ts # Weekly notice: pings Community Zeus in #side-ops when the upcoming Friday has no op
    signup.ts           # Signup service logic
    transferRequests.ts # Polls assignment transfer requests awaiting leader decision
    transferDelivery.ts # Transfer leader-request / rejection / info delivery DMs

  lib/
    roles.ts            # isStaff check
    resolve.ts          # discordId ↔ Payload user lookups
    transferMessaging.ts # Transfer embed builders + delivery keys

Command registration scope

  • Global (DM-usable): signup, link, ping
  • Guild-only: announce, remind-evaluations (guild-scoped commands never appear in DMs)

/signup is DM-only. Creates Payload user with username = discordUsername = caller's Discord username. Ephemeral reply carries temp password (guaranteed delivery path); interaction.user.send() is best-effort persistent copy. /link works in servers and DMs — matches discordUsername → sets discordId.

Feature flow: attendance

Bot posts RSVP embeds (Yes/Tentative/No) for future, Ready/Scheduled, visibility:"unit" missions into ops channel. Stores discordMessageId + discordAttendanceHash on mission. Reconciles hash changes every poll tick (web ↔ Discord two-way sync). The embed title links to the mission page, a mission summary sits under the title (truncated to 300 characters with an ellipsis), and an "Open the op" field carries the URL; attendanceHash includes an embed schema version so layout-only changes re-render live roll-calls once (idempotent). Web UI: MissionAttendance component.

Feature flow: notifications

notificationBridge polls user-notifications (cursor = last seen id; cap 5 DMs/tick). DMs opted-in users (preferences.discord.enabled, not in mutedTypes).

Feature flow: evaluation reminders

Once a mission is evaluable (Completed, or Scheduled/Active past its start), sends a one-shot DM per linked user asking them to rate their leadership (leaders also get a "rate your subordinates" section). Recipient plan: computeEvaluationReminderPlan in src/lib/evaluations/reminders.ts; state marker evaluationRemindersSentAt on Missions (withheld if the 40-DM/tick cap is hit — retries next tick). DMs ignore preferences.discord.enabled (defaults false, not yet exposed in web UI). Staff can re-send manually via /remind-evaluations.

Feature flow: imminent op reminder

15 minutes before a posted op starts (visibility: "unit", Ready/Scheduled, live roll-call message), imminentReminders opens a thread on the roll-call message ("Op starting soon: ") and pings every yes-RSVP via <@discordId> (pings ignore preferences.discord.enabled, same exception as evaluation reminders). One-shot via the imminentReminderSentAt marker on Missions; the cancel flow clears it so a rescheduled op re-reminds. Grace bound: a missed reminder still fires within 15 minutes after the start, never later. Query: src/lib/intelligence/imminentReminders.ts (window constants + getImminentReminderMissions).

Feature flow: Friday open-slot notice

At 8 PM Eastern on Wednesday (America/New_York, evaluated via isNoticeDue in src/lib/intelligence/fridayOpsNotice.ts; catch-up until ET midnight, a fully missed Wednesday skips to next week), if no non-cancelled mission starts on that week's upcoming Friday, fridayOpsNotice posts once in #side-ops pinging the Community Zeus role: the slot is open, hosting is welcome, and if nobody picks it up the slot stays empty or Jason hosts it himself. When a non-superuser creator later claims the slot (getFridayClaimant, earliest-created mission wins; superuser/staff-created missions never claim), the notice is edited to append "This slot was claimed by <user>. Thank you for hosting for our community!"; if that op is later cancelled or deleted, the claim line is stripped and the notice reverts to the open-slot state (a between-polls claim swap re-credits the new claimant). Role: DISCORD_COMMUNITY_ZEUS_ROLE_ID or resolved by name in the guild; channel: DISCORD_SIDE_OPS_CHANNEL_ID. Dedup without DB state: the message embeds a stable per-Friday <t:epoch:D> token and the service scans recent channel messages for it (claim idempotency via a marker in the message content). Drafts (Concept) count as the slot being taken.

Feature flow: assignment transfers

transferRequests polls assignment-transfers for requests awaiting leader decision; leaders approve/deny via buttons handled in transferInteractions.ts; DMs are built by lib/transferMessaging.ts and sent through transferDelivery.ts (leader request, requester rejection notice, decision info). Web side: src/lib/transfers/ + /transfers page.

Where to look

Task Path
Add new slash command bot/commands/<name>.ts + register in bot/commands/index.ts
Add button interaction bot/events/interactionCreate.ts (RSVP) or transferInteractions.ts (transfers)
Modify embed lifecycle bot/services/missionEmbeds.ts
Change DM bridging bot/services/notificationBridge.ts
Evaluation reminder logic bot/services/evaluationReminders.ts + src/lib/evaluations/reminders.ts
Imminent-op reminder logic bot/services/imminentReminders.ts + src/lib/intelligence/imminentReminders.ts
Friday open-slot notice bot/services/fridayOpsNotice.ts + src/lib/intelligence/fridayOpsNotice.ts
Transfer workflow bot/services/transferRequests.ts + lib/transferMessaging.ts
User lookup patterns bot/lib/resolve.ts

Anti-patterns

  • NEVER run bot and web app with separate database connections without connection pooling — they share PostgreSQL
  • NEVER register guild-only commands if the command needs to work in DMs (signup, link, ping must be global)
  • NEVER assume DM delivery succeeded — /signup uses ephemeral reply as primary delivery path
  • NEVER modify discordId directly in Payload admin — use the /link command or the resolve helper