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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ SLACK_SIGNING_SECRET=your-signing-secret
SLACK_APP_TOKEN=xapp-your-app-token
SLACK_BOT_TOKEN=xoxb-your-bot-token

# Web
SLACK_CLIENT_ID=your-slack-app-client-id
SLACK_CLIENT_SECRET=your-slack-app-client-secret
# openssl rand -base64 48
BETTER_AUTH_SECRET=replace-with-at-least-32-random-characters
# public origin the dashboard and API are served from
DASHBOARD_BASE_URL=https://prometheus.hackclub.com
# PORT=3000

# PostgreSQL
DATABASE_URL=postgresql://prometheus:password@postgres:5432/prometheus
# DATABASE_POOL_SIZE=4
Expand Down
49 changes: 48 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Prometheus is a Slack bot built with `@slack/bolt` in Socket Mode. It runs via `
- `canAnchor` — `canManage` OR workspaceAdmin
- `SUPERADMINS` grants access to the `/pro admin` command; it does not automatically insert rows into `global_admins`

**Database** (`lib/db.js`): Uses Bun's native pooled PostgreSQL client, defaulting to four connections per bot instance. The Drizzle schema is `lib/db/schema.ts`; generated migrations live in `drizzle/` and must be applied with `bun run db:migrate` before starting a new application version. Tables are `global_admins`, `appointed_managers`, `channel_bans`, `join_messages`, `embed_blocks`, `anchor_polls`, `anchor_poll_choices`, `anchor_poll_votes`, and `anchor_nps_responses`.
**Database** (`lib/db.js`): Uses Bun's native pooled PostgreSQL client, defaulting to four connections per bot instance. The Drizzle schema is `lib/db/schema.ts`; generated migrations live in `drizzle/` and must be applied with `bun run db:migrate` before starting a new application version. Tables are `global_admins`, `appointed_managers`, `channel_bans`, `join_messages`, `embed_blocks`, `anchor_polls`, `anchor_poll_choices`, `anchor_poll_votes`, `anchor_nps_responses`, and `channel_api_keys`.

All exported database functions are asynchronous and must be awaited. Multi-row anchor creation and vote toggling use transactions and advisory locks so overlapping bot instances remain consistent during rolling deploys. Keep schema changes additive and safe for old and new application versions to run concurrently. Generate and commit migrations, do not try to manually create or edit drizzle migrations.

Expand All @@ -51,6 +51,48 @@ All exported database functions are asynchronous and must be awaited. Multi-row

**Rate limiter** (`lib/ratelimiter.js`): Handles Slack API rate limits with exponential backoff. Used by `lib/purge.js` for batch message deletion.

**Web dashboard and API** (`lib/web/`): A Hono app served by `lib/web/server.js`, mounted by
`index.js` alongside the bot and also runnable on its own with `bun run start:web`. Sign-in is
Slack OAuth through Better Auth (`auth.js`), restricted to the workspace the bot is installed in.

- `app.js` — routing, security headers, and same-origin checks on every session mutation
- `api.js` — the public, bearer-authenticated `/api/v1` router
- `apiKeys.js` — key minting, hashing, and revocation
- `permissions.js` — maps a Slack user to the channels they can configure
- `sections.js` — the single source of truth for dashboard navigation
- `views.jsx` — hono/jsx server-rendered pages; `dashboard.css` is the only asset

The dashboard is deliberately scoped to API keys right now. The rest of the first pass is parked,
not deleted: nav entries live in `PARKED` in `sections.js`, page components in `views.parked.jsx`,
and their mutation helpers in `channelSettings.js`. Bringing a section back means moving its `PARKED`
entry into `NAV`, adding it to the `pages` map in `views.jsx`, and re-registering its POST routes in
`app.js`. Nothing imports `views.parked.jsx` or `channelSettings.js` in the meantime.

There is no JavaScript on the dashboard — the CSP forbids it — so every interaction is a plain form
POST. Key creation renders its response directly instead of redirecting, because the plaintext key
exists only for that one response.

**API keys** (`lib/web/api.js`): `POST /api/v1/messages/delete` deletes messages programmatically.
Keys are scoped to one channel and one owner, stored only as a SHA-256 hash, and re-checked against
`canManage` on every request, so demoting an owner disables their keys without an explicit
revocation. Deletions reuse `logDelete`/`publicLogDelete` and are attributed to the key's owner.
Per-key throughput is capped at 60 requests per minute in memory, and batches at 50 messages.

Two properties are load-bearing. `LOG_CHANNEL` is mandatory for the endpoint: `logDelete` treats an
unset channel as a successful no-op, so without this guard the API would delete messages with no
record at all. And authorization is rechecked per message rather than once per request, so revoking a
key or demoting its owner halts a batch already in flight. Request bodies are counted in bytes as
they stream and capped at 64 KB, because `Content-Length` is absent under chunked encoding. The
`reason` has its angle brackets stripped — it lands in a mrkdwn block, where `<!channel>` would
otherwise fire a real broadcast ping in the audit channel.

A user may hold `MAX_API_KEYS_PER_USER` (5) active keys in total, across every channel, not per
channel. `createChannelApiKey` counts and inserts inside one transaction behind an advisory lock on
the user id, so concurrent creations cannot both pass the check; it returns `null` at the cap and
`createApiKey` turns that into a user-facing error. The per-key rate limit is a loose temporary
guard, not a quota: Slack's tier 3 limit is the real ceiling and key holders already have the same
power via the Slack shortcut.

## Environment Variables

| Variable | Purpose |
Expand All @@ -67,6 +109,11 @@ All exported database functions are asynchronous and must be awaited. Multi-row
| `HACKCLUB_CDN_KEY` | CDN API key for archiving deleted threads (optional) |
| `SLACK_BROWSER_TOKEN` | Browser token (xoxc) for undocumented moderation APIs (optional) |
| `SLACK_COOKIE` | Session cookie (`d=` value) paired with browser token (optional) |
| `SLACK_CLIENT_ID` | Slack app client ID for dashboard sign-in (optional) |
| `SLACK_CLIENT_SECRET` | Slack app client secret for dashboard sign-in (optional) |
| `BETTER_AUTH_SECRET` | Random 32+ character secret for Better Auth sessions (optional) |
| `DASHBOARD_BASE_URL` | Public HTTPS origin the dashboard and API are served from (optional) |
| `PORT` | Port the web server listens on (optional, defaults to `3000`) |

## Adding a New Command

Expand Down
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ COPY . .

ENV NODE_ENV=production

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD bun -e 'try { const response = await fetch("http://127.0.0.1:3000/health"); process.exit(response.ok ? 0 : 1); } catch { process.exit(1); }'
CMD bun -e 'try { const port = process.env.PORT || 3000; const response = await fetch(`http://127.0.0.1:${port}/health`); process.exit(response.ok ? 0 : 1); } catch { process.exit(1); }'

CMD ["bun", "run", "index.js"]
61 changes: 47 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,34 @@ Prometheus is a Slack bot that lets community members take responsibility for ke
- **Channel manager**: appointed per-channel; can delete, destroy, set welcome messages
- **Channel moderator**: appointed per-channel; can timeout, @here, @channel

## Web API

Prometheus offers an web API to programmatically execute moderation actions. You can use this API how you see fit, common use cases include automod bots, cleaning up certain bot messes, or anything to your hearts content.

Keys are scoped to **one channel and one user**. You can hold up to five keys at once, counted across every channel; a key only ever works in the channel it was made for, and it explodes the moment its owner loses their channel manager role. Any actions done via API keys is still logged in the same way as if the user had done it themselves.

```bash
curl -X POST https://prometheus.hackclub.com/api/v1/messages/delete \
-H "Authorization: Bearer $PROMETHEUS_KEY" \
-H "Content-Type: application/json" \
-d '{"ts":"1699999999.123456","reason":"spam"}'
```

```json
{ "ok": true, "channel": "C0123ABCD", "deleted": ["1699999999.123456"], "failed": [] }
```

| Field | Required | Notes |
| -------- | -------- | ----------------------------------------------- |
| `ts` | Yes | One message timestamp, or an array of up to 50 |
| `reason` | Yes | Up to 500 characters, recorded in the audit log |

A `200` means the request was accepted, not that every message was deleted, so check `deleted` and `failed`, where each failure carries the Slack error (`message_not_found`, `cant_delete_message`).

Other statuses: `400` malformed request, `401` missing or invalid key, `403` no permission, `413` request body over 64 KB, `429` over the rate limit of 60 requests per minute.

`GET /api/v1/key` just returns a key's metadata, which is handy for checking the status of a key.

## Setup

> **⚠️ Workspace admin perms required.** Only cloning the repo and installing the Slack app is **not** enough. Almost every moderation feature (delete message, destroy thread, timeout/kick, move members, ban enforcement, clear embeds, and similar) runs through `SLACK_USER_TOKEN` and **will not work** unless that token is a User OAuth token (`xoxp`) from a **workspace admin**. Without it, only trivial commands like `ping`, `coin`, and `help` work.
Expand All @@ -48,20 +76,25 @@ Prometheus is a Slack bot that lets community members take responsibility for ke
4. Create an app-level token with `connections:write` (for Socket Mode!).
5. Fill out your `.env`, check the `.env.example` for reference. Here's a bit more detailed rundown of what to expect

| Variable | Required | Purpose |
| ---------------------- | -------- | ------------------------------------------------------------------------------------------------- |
| `SLACK_BOT_TOKEN` | Yes | Bot User OAuth Token (xoxb) for posting messages |
| `SLACK_USER_TOKEN` | Yes | User OAuth Token (`xoxp`) from a **workspace admin** — required for all moderation actions |
| `SLACK_APP_TOKEN` | Yes | App-Level Token (xapp) with `connections:write` for Socket Mode |
| `SLACK_SIGNING_SECRET` | Yes | Signing secret from app settings |
| `SUPERADMINS` | Yes | Comma-separated Slack user IDs seeded as global admins (e.g. `U12345678,U87654321`) |
| `DATABASE_URL` | Yes | PostgreSQL connection URL |
| `DATABASE_POOL_SIZE` | No | Maximum PostgreSQL connections per bot instance (defaults to 4) |
| `LOG_CHANNEL` | No | Channel ID for **private** audit logs which includes full message content and CDN transcripts |
| `PUBLIC_LOG_CHANNEL` | No | Channel ID for **public** audit logs which are redacted, shows only who did what in which channel |
| `HACKCLUB_CDN_KEY` | No | CDN API key for archiving deleted thread archives to the HC CDN |
| `SLACK_BROWSER_TOKEN` | No | Browser token (xoxc) for Slack's undocumented moderation APIs (eg thread hiding) |
| `SLACK_COOKIE` | No | Session cookie (`d=` value) paired with `SLACK_BROWSER_TOKEN` |
| Variable | Required | Purpose |
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------- |
| `SLACK_BOT_TOKEN` | Yes | Bot User OAuth Token (xoxb) for posting messages |
| `SLACK_USER_TOKEN` | Yes | User OAuth Token (`xoxp`) from a **workspace admin** — required for all moderation actions |
| `SLACK_APP_TOKEN` | Yes | App-Level Token (xapp) with `connections:write` for Socket Mode |
| `SLACK_SIGNING_SECRET` | Yes | Signing secret from app settings |
| `SUPERADMINS` | Yes | Comma-separated Slack user IDs seeded as global admins (e.g. `U12345678,U87654321`) |
| `DATABASE_URL` | Yes | PostgreSQL connection URL |
| `DATABASE_POOL_SIZE` | No | Maximum PostgreSQL connections per bot instance (defaults to 4) |
| `LOG_CHANNEL` | API | Channel ID for **private** audit logs which includes full message content and CDN transcripts |
| `PUBLIC_LOG_CHANNEL` | No | Channel ID for **public** audit logs which are redacted, shows only who did what in which channel |
| `HACKCLUB_CDN_KEY` | No | CDN API key for archiving deleted thread archives to the HC CDN |
| `SLACK_BROWSER_TOKEN` | No | Browser token (xoxc) for Slack's undocumented moderation APIs (eg thread hiding) |
| `SLACK_COOKIE` | No | Session cookie (`d=` value) paired with `SLACK_BROWSER_TOKEN` |
| `SLACK_CLIENT_ID` | No | Slack app client ID used by Sign in with Slack |
| `SLACK_CLIENT_SECRET` | No | Slack app client secret used by Sign in with Slack |
| `BETTER_AUTH_SECRET` | No | Random secret of at least 32 characters used by Better Auth |
| `DASHBOARD_BASE_URL` | No | Public HTTPS origin the dashboard and API are served from (e.g. `https://prometheus.hackclub.com`) |
| `PORT` | No | Port the dashboard and API listen on (defaults to 3000) |

6. Apply database migrations and run it:

Expand Down
Loading