7.8 KiB
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.