Skip to content

feat(bot): Discord bot workspace with /meet link and /meet create - #188

Merged
Anuraj-dev merged 6 commits into
mainfrom
feat/discord-bot-workspace
Jul 23, 2026
Merged

Anuraj-dev merged 6 commits into
mainfrom
feat/discord-bot-workspace

Conversation

@Anuraj-dev

Copy link
Copy Markdown
Owner

Ticket 2 of 2 of the Discord bot feature (spec: `docs/specs/2026-07-23-discord-bot-design.md`). Ticket #185 (the server integration API) is already merged; this adds the thin Discord-facing adapter that calls it.

What's here

  • New `bot/` workspace (4th monorepo workspace) — strict TS + Vitest + ESLint mirroring server/client, own `package-lock.json`, wired into root workspaces and scripts.
  • discord.js process, slash commands only (no Message Content intent):
    • `/meet link` → `POST /link-token`, replies ephemerally with the confirmation URL.
    • `/meet create` → `POST /rooms`; on success posts a public embed with the join link (`<CLIENT_URL>/lobby/`, matching the client's own `buildJoinUrl`) and "Started by @user"; `not_linked` → ephemeral prompt to run `/meet link`; other failures → friendly ephemeral error. Never silent.
  • Typed HTTP client for the two bot-facing endpoints (`X-Bot-Api-Key` auth, typed `NotLinkedError`/`IntegrationHttpError`), using the `@a-meet/contracts` DTOs.
  • Slash-command registration script (`npm --prefix bot run register`; guild if `DISCORD_GUILD_ID` set, else global).
  • CI: `bot-lint` + `bot` (tests w/ coverage ratchet) jobs in ci.yml; aggregate `typecheck.yml` already covers bot.
  • Prod wiring: `bot` service in `docker-compose.prod.yml` (`restart: always`, awslogs, host network) + `bot/Dockerfile`.
  • Docs: `bot/README.md` + `bot/.env.example` (no secrets committed).

Testing (strict TDD, behavior not implementation)

28 unit tests, no real Discord: command handlers with mocked interactions + mocked HTTP client (linked / not_linked / server-error / network-error, ephemeral vs public reply routing asserted); HTTP client against a mocked fetch (headers, body, base-URL, status→result mapping); env loader; interaction router. Coverage 100% stmts/funcs/lines, 96% branches.

Bot holds no meeting logic — thin adapter only. Runs independently of the API server.

Closes #186

Add a 4th monorepo workspace, bot/, a thin discord.js adapter over the
/api/integrations/discord API (ticket #185). Slash commands only (no
Message Content intent):

- /meet link  -> mints an account-link token, replies ephemerally with the
  confirmation URL.
- /meet create -> creates an instant room hosted by the linked user and posts
  a public embed with the join link; not-linked users get an ephemeral prompt
  to run /meet link; other failures reply with a friendly ephemeral error.

Includes a typed HTTP client for the two bot-facing endpoints (X-Bot-Api-Key
auth, not_linked detection), a slash-command registration script, strict TS +
Vitest + ESLint mirroring the other workspaces, per-workspace lockfile, CI
lint/test jobs, aggregate typecheck coverage, a production Dockerfile, and a
bot service wired into docker-compose.prod.yml (restart: always).

Closes #186
@vercel

vercel Bot commented Jul 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
a-meet Ready Ready Preview, Comment Jul 23, 2026 8:01am

Address review findings:

- Handlers defer the reply immediately (deferReply ephemeral) before the HTTP
  call, then editReply/followUp — Discord invalidates the interaction token
  after ~3s, so a slow API call no longer leaves the user with a dead
  interaction. Success posts the public embed as a non-ephemeral followUp while
  errors/not-linked stay ephemeral, preserving the required reply routing.
- docker-compose.prod.yml: the bot service is now profile-gated (profiles:
  [bot]) so the automated server deploy's bare `up -d` never touches it, and
  its env uses non-failing `:-` defaults instead of `:?` guards (which also
  fixes invalid YAML from a colon-space in the guard message). The bot validates
  its own required config at startup and exits non-zero if misconfigured.
- Root .dockerignore now allowlists bot/package.json + bot/src so the bot image
  (built from the repo-root context) resolves its COPYs; removed the dead
  bot/.dockerignore (a per-Dockerfile ignore is not applied for a root context).
- bot/Dockerfile drops the unused tsconfig copies (tsx strips types without them).
…ublic-boundary test

- handleCreate now wraps the public followUp in try/catch: if posting the
  announcement fails (e.g. the bot can't send in that channel), it resolves the
  ephemeral deferred reply with the meeting link instead of letting the error
  bubble to the InteractionCreate handler, which skips already-deferred
  interactions and would leave the invoker hanging.
- handlers test asserts the public followUp sets neither the ephemeral flag bit
  nor the deprecated `ephemeral: true` shorthand, and adds a rejecting-followUp
  case.
- Extract dispatchInteraction: a top-level safety net around the router that
  recovers ALREADY-deferred/replied interactions via editReply (the previous
  guard only handled the not-yet-deferred case, so a mid-handler editReply/
  followUp rejection left the invoker with an unresolved spinner). bot.ts now
  delegates to it. Added tests for the deferred, not-deferred, failing-recovery,
  and non-repliable paths.
- Harden the public-boundary test helper to reject the Ephemeral marker in every
  form discord.js accepts (numeric bit, 'Ephemeral' string, array) plus the
  deprecated ephemeral:true shorthand.
- Root coverage + audit aggregates and the CI audit job now include the bot
  workspace, so its tests and dependency tree are inside the pre-merge gates.
A server that accepts the connection but never completes the response would
otherwise leave a deferred /meet interaction hanging with no exception for
dispatchInteraction to recover from. Each request now runs under an
AbortController deadline (default 10s, configurable); on timeout the fetch
rejects and the handler answers with a friendly ephemeral error. Tested with a
never-resolving fetch under fake timers plus a signal-passing assertion.
Advance fake timers past the deadline after a successful request and assert the
signal stays un-aborted, so the test would fail if clearTimeout were removed.
@Anuraj-dev
Anuraj-dev merged commit 50c8e34 into main Jul 23, 2026
16 checks passed
@Anuraj-dev
Anuraj-dev deleted the feat/discord-bot-workspace branch July 23, 2026 08:17

This branch was successfully deployed

1 active deployment
Preview — 23e35c1e Deployed Jul 23, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discord bot (2/2): bot/ workspace with /meet link and /meet create

1 participant