Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# The server image only needs workspace manifests plus server/shared sources.
# The server and bot images share this repository root as their build context,
# so Docker applies THIS ignore file (a per-Dockerfile bot/.dockerignore would be
# ignored). Allowlist exactly the workspace manifests + sources each image needs.
**
!package.json
!package-lock.json
Expand All @@ -8,6 +10,12 @@ server/**
!server/Dockerfile
!server/src/
!server/src/**
!bot/
bot/**
!bot/package.json
!bot/Dockerfile
!bot/src/
!bot/src/**
!shared/
shared/**
!shared/package.json
Expand Down
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,41 @@ jobs:
- run: npm ci --ignore-scripts
- run: npm run lint

bot-lint:
name: Bot lint
runs-on: ubuntu-latest
defaults:
run:
working-directory: bot
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: bot/package-lock.json
# ESLint needs no native build, so --ignore-scripts keeps install fast.
- run: npm ci --ignore-scripts
- run: npm run lint

bot:
name: Bot tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: bot
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: bot/package-lock.json
- run: npm ci --ignore-scripts
# Run under coverage so the non-decreasing ratchet (thresholds in
# vitest.config.ts) gates every PR — a coverage drop fails this check.
- run: npm run test:coverage

server:
name: Server tests
runs-on: ubuntu-latest
Expand Down Expand Up @@ -81,6 +116,10 @@ jobs:
working-directory: client
- run: npm audit --audit-level=high
working-directory: client
- run: npm ci --ignore-scripts
working-directory: bot
- run: npm audit --audit-level=high
working-directory: bot

client:
name: Client tests + build
Expand Down
25 changes: 25 additions & 0 deletions bot/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# A-Meet Discord bot — local configuration sample.
# Copy to bot/.env and fill in. NEVER commit real values; in production these are
# injected by the container environment (SSM SecureStrings for the secrets).

# ── Discord application credentials ─────────────────────────────────────────
# From the Discord Developer Portal (https://discord.com/developers/applications).
# DISCORD_TOKEN is the bot token (Bot → Reset Token). SECRET — never commit/log.
DISCORD_TOKEN=
# The application (client) id — Discord Developer Portal → General Information.
DISCORD_CLIENT_ID=
# Optional: register slash commands to a single guild for instant updates while
# developing. Leave empty to register globally (can take up to ~1h to propagate).
DISCORD_GUILD_ID=

# ── A-Meet endpoints ────────────────────────────────────────────────────────
# Base URL of the A-Meet API server the bot calls. Local: http://localhost:5000.
SERVER_URL=http://localhost:5000
# Base URL of the A-Meet web client, used to build public meeting join links.
CLIENT_URL=http://localhost:5173

# ── Integration auth ────────────────────────────────────────────────────────
# Shared secret sent to /api/integrations/discord/* in the X-Bot-Api-Key header.
# MUST match the server's DISCORD_BOT_API_KEY. HOST-GRADE secret — never commit;
# in prod this is an SSM SecureString.
DISCORD_BOT_API_KEY=
39 changes: 39 additions & 0 deletions bot/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# syntax=docker/dockerfile:1
#
# Production image for the A-Meet Discord bot — a small discord.js process that
# adapts slash commands to the /api/integrations/discord API. No native builds
# (unlike the server's mediasoup), so the image is a plain Node runtime.

# ---- build stage: install the bot workspace from the root lockfile ----
FROM node:22-bookworm-slim AS build
WORKDIR /app

# Install the bot workspace and its shared-contract dependency from the root
# lockfile. The image build context is the repository root so workspace links
# resolve exactly as they do in CI and local development.
COPY package.json package-lock.json ./
COPY bot/package.json ./bot/package.json
COPY shared/package.json ./shared/package.json
RUN npm ci --omit=dev --workspace bot --ignore-scripts

COPY bot/src ./bot/src
COPY shared/src ./shared/src

# ---- runtime stage: slim image carrying only the built artifacts ----
FROM node:22-bookworm-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app/bot

COPY --from=build /app/node_modules /app/node_modules
COPY --from=build /app/bot ./
COPY --from=build /app/shared /app/shared
COPY --from=build /app/package.json /app/package.json
COPY --from=build /app/package-lock.json /app/package-lock.json

RUN chown -R node:node /app
USER node

# Run through the tsx loader: sources are TypeScript and tsx strips types at load
# time, so the image needs no separate compile step. tsx ships as a runtime
# dependency (kept by --omit=dev).
CMD ["node", "--import", "tsx", "src/index.ts"]
52 changes: 52 additions & 0 deletions bot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# A-Meet Discord bot

A small [discord.js](https://discord.js.org) process that lets Discord users create
A-Meet meetings from a channel. It is a **thin adapter** — it holds no meeting logic
and talks to the A-Meet server only through the `/api/integrations/discord` API
(authenticated with a shared bot API key). The bot and the API server are separate
processes: a Discord outage never affects the API and vice-versa.

## Commands (slash-only, no Message Content intent)

- **`/meet link`** — one-time Discord ↔ A-Meet account linking. Replies **ephemerally**
(only the invoker sees it) with a short-lived confirmation URL to open in the browser
while signed in to A-Meet.
- **`/meet create`** — creates an instant meeting hosted by the linked user and posts a
**public embed** with the join link (`<CLIENT_URL>/lobby/<roomId>`) and "Started by @user".
If the account isn't linked yet, replies ephemerally prompting `/meet link`.

## Configuration

Copy `.env.example` to `.env` and fill it in. Variables:

| Var | Required | Purpose |
|---|---|---|
| `DISCORD_TOKEN` | yes | Bot token (secret) used to log the gateway client in. |
| `DISCORD_CLIENT_ID` | yes | Application id, needed to register slash commands. |
| `DISCORD_GUILD_ID` | no | If set, registers commands to that guild (instant, dev-friendly); else global. |
| `SERVER_URL` | yes | Base URL of the A-Meet API server. |
| `CLIENT_URL` | yes | Base URL of the web client, used to build meeting links. |
| `DISCORD_BOT_API_KEY` | yes | Shared secret (`X-Bot-Api-Key`); must match the server's value. Secret. |

Secrets are never committed. In production they come from SSM SecureStrings via the
container environment.

## Scripts

```bash
npm --prefix bot run register # register/update the /meet slash command with Discord
npm --prefix bot run dev # run with reload (tsx watch)
npm --prefix bot start # run once
npm --prefix bot test # unit tests (no real Discord)
npm --prefix bot run typecheck
npm --prefix bot run lint
```

## First-time setup

1. Create an application + bot in the [Discord Developer Portal](https://discord.com/developers/applications);
copy the **bot token** and **application id** into `.env`.
2. Invite the bot to a server with the `applications.commands` (and `bot`) scopes.
3. `npm --prefix bot run register` to publish the `/meet` command.
4. Set `DISCORD_BOT_API_KEY` to the same value configured on the server.
5. `npm --prefix bot start`.
35 changes: 35 additions & 0 deletions bot/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import globals from 'globals';
import { defineConfig, globalIgnores } from 'eslint/config';
import { baseEslintConfigs } from '../eslint.config.base.mjs';

// Flat config for the Discord bot (ESLint 10), mirroring the server setup.
// Lints application source, tests, and config files with ESM parsing and Node
// globals; test files additionally get Vitest's globals.
export default defineConfig([
globalIgnores(['node_modules', 'coverage', 'dist']),
{
files: ['**/*.js'],
extends: [baseEslintConfigs.javascript],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: globals.node,
},
},
{
files: ['**/*.ts'],
extends: [baseEslintConfigs.javascript, baseEslintConfigs.typescript],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: globals.node,
},
},
{
// Vitest exposes describe/it/expect/vi etc.; scope those to test files only.
files: ['test/**/*.{js,ts}', '**/*.test.{js,ts}'],
languageOptions: {
globals: { ...globals.node, ...globals.vitest },
},
},
]);
Loading
Loading