Files
minabot/src/discord/README.md

7.8 KiB
Raw Blame History

Discord interface

Signed HTTP interactions enter /api/discord/interactions. The raw request body is verified with the application's Ed25519 public key and a five-minute timestamp window. DISCORD_PUBLIC_KEY enables the endpoint; DISCORD_CLIENT_ID identifies the application. Commands are global with contexts [0, 1] (server and bot DM).

HabitService remains the shared domain implementation. Verified Discord user IDs resolve the same account as website OAuth; new accounts start in UTC. Server responses are ephemeral. Buttons/forms carry opaque persisted owner-bound state, expire after 15 minutes, and mutation controls are consumed after successful use. Interaction IDs retain replay receipts for 24 hours. Neither table stores tokens.

daily.ts implements the /today workflow. types.ts, ui.ts, and state.ts provide the shared feature building blocks. Feature tasks extend command registration and route dispatch while keeping their behavior in separate modules.

Tests use real Ed25519 signatures and disposable migrated SQLite databases. The foundation tests cover server/DM privacy, replay after restart, stale progress, cross-account controls, and matching website state. All feature modules are now composed in the shared API. End-to-end user interaction in Discord remains a manual acceptance check after deployment and registration.

/help lists the current command definitions automatically, explains check-in controls and privacy, and links to the website. It works before account setup and does not create an account or synchronize habits. Re-run the registration script with --apply after deployment to expose newly added commands in Discord. Reminder delivery accepts commandsAvailable; production enables it when DISCORD_PUBLIC_KEY is a 64-digit hexadecimal key and DISCORD_CLIENT_ID is set. The existing bot token still controls delivery. Configured reminders include an owner-bound today control with mode: 'reminder': it opens a separate private flow, leaving the reminder unchanged, and expires at the sending account's next local midnight (including DST). Delivery claims, nonce, opt-out, and retry rules are unchanged. The dashboard preview explicitly disables buttons because it has no signed interaction endpoint configured. Global command registration and the Discord interaction endpoint must be configured separately before production use.

Preferences

preferences.ts implements global /settings and /reminders. Reminder menus paginate ten named schedules at a time; entries with matching name and schedule are edited together as multiple comma-separated 24-hour times. Forms accept weekday names (or every day) or one ISO calendar date. Delivery is toggled for all schedules. Removing the last schedule disables delivery and stores an empty list; the shared contract permits empty lists only while disabled. Invalid forms provide a prefilled retry button. UTC accounts receive a timezone setup hint.

ReminderService is shared with the website routes. Every save advances its updatedAt monotonically; Discord controls compare the saved preference snapshot and the user's timezone/update version before editing. HabitService.setTimezone also advances the user timestamp monotonically, including repeated writes in the same millisecond. This rejects old Discord forms after edits from either interface, including change-and-revert sequences. Website PUT retains its existing request contract. Delivery receipts do not invalidate preference controls.

Integration: retain the preferences command dispatch and preferencesAction component dispatch in routes.ts, the two definitions in commands.ts, and the preference fields in ControlData. CommandConfig.remindersAvailable supplies availability; createApi derives it from the existing sharing bot token, matching the website and reminder worker. Direct route fixtures default it to false. No new environment variables, migrations, outbound requests, or remote command registration are needed by this module. Global command registration remains an integration step. Successful mutations invoke the supplied live-card dirty hook. progress.ts implements /progress: six-habit text pages, focused private previews, explicit destination confirmation for snapshots, and live-image start/status/pause/resume. API composition injects the same sharing.send and live.start/pause/state operations used by website routes. Keep these shared instances when reconciling dispatch; do not construct a second live worker.

Rendering and network actions return ephemeral type-5 acknowledgements and edit the original interaction response once. DeferredDispatch.complete and schedule are injectable for tests. Interaction tokens live only in memory, never receipts or controls. A process interruption after acknowledgement can leave the private response unfinished; restarting never replays the public send. Run /progress again to inspect status. Public snapshot and live deliveries retain their existing persistent quotas, nonce and uncertain-delivery guards.

progress-image.ts renders the focused 30-day snapshot at 1200×860 with bundled Mina Sans / Instrument Serif, shared theme colors, and existing habit shades. The 30 cells retain fixed readable sizes; symbols are Canvas paths. Text summaries and buttons accompany the PNG. To produce review images without sending messages: PROGRESS_IMAGE_REVIEW_DIR=/tmp/minabot-progress-review bun test src/discord/progress-image.test.ts.

Integrated commands

All commands use global registration with server and bot-DM contexts. /help reads this same registration metadata, so its command list stays in sync.

Command Function
/help Command guide and website link
/today Daily manual/count/task check-ins
/habits Create, edit, schedule, archive, restore, permanently delete
/tasks Add, rename, schedule, delete routine tasks
/history Inspect and correct dated progress, including archived habits
/reminders Named schedules, multiple times, quiet hours, enable/disable
/settings Timezone and reminder delivery status
/progress Private graphics, confirmed snapshot sharing, live-image controls

After the deployment controller installs a release, register its definitions:

cd /opt/minabot
sudo docker compose exec -T app bun scripts/register-discord-commands.ts --apply

Registration upserts individual global commands and preserves unrelated commands. The public key and interactions URL are one-time setup; adding commands requires rerunning registration after their handlers have deployed.

Additional command graphics

/today includes View day card, summarizing all due habits in pages of six. The selected daily check-in and /history editor include View check-in card, showing the selected date's requirements, task completion, carryover where applicable, and a 14-day progress strip ending on that date. /reminders includes View schedule card, showing each time slot, weekday/date schedule, quiet-hour conflicts, and delivery status. Schedule cards retain disabled/unavailable states. Management forms and /help remain text-based because their controls already communicate the relevant choices.

These are private snapshots: Refresh card, Toggle theme, and pagination request a fresh owned snapshot. Regular check-ins keep their existing immediate text responses. Graphic actions acknowledge before rendering and attach a PNG to a new private reply; they never publish to the sharing channel or write progress. Each image has a text equivalent and attachment description. A render failure retains the text and navigation. No new command registration or configuration is needed after deploying this change.

Generate local visual review artifacts with COMMAND_IMAGE_REVIEW_DIR=/tmp/minabot-command-review bun test src/discord/command-graphics.test.ts.