Building Slack integration

This commit is contained in:
KS Jannette
2026-08-02 06:13:01 -04:00
parent dae256f6c6
commit b607ee9121
16 changed files with 2351 additions and 8 deletions

View File

@@ -73,8 +73,11 @@ The shared infrastructure that third-party integrations are built on. See [ARCHI
| --- | --- | --- |
| `POST /v1/notes` | `Authorization: Bearer <api key>` | Bulk push notes. Body `{ notes: [{ id, text, author, x?, y?, color? }] }`, max 5000 per request. |
| `POST /v1/webhooks/:provider` | Per-provider request signature | Inbound provider deliveries. Acks immediately, inserts in the background. |
| `POST /v1/slack/commands` | Slack `v0` request signature | The `/sticky` slash command. Form-encoded rather than JSON, and its response body is shown to the user in Slack. |
Registered providers live in `backend/config/providers.ts`. `rest` and `linear` carry payload mappings today; `slack`, `github`, and `jira` declare transport and OAuth endpoints only, so adding one of those integrations means writing a single normalizer rather than new plumbing.
Registered providers live in `backend/config/providers.ts`. `rest`, `linear`, and `slack` carry payload mappings today; `github` and `jira` declare transport and OAuth endpoints only, so adding one of those integrations means writing a single normalizer rather than new plumbing.
A provider may also declare an async `enrich` hook. It runs inside the retry queue *after* the request is acknowledged, which is where any network call belongs: normalizers stay pure and synchronous so a delivery can be acked inside Slack's three-second budget. Slack needs this because a reaction event names a message without carrying its text.
Deliveries are deduplicated on `(provider, external_id)` in the `ingest_events` table, which matters beyond tidiness: duplicate notes compress intra-cluster distance and depress the cohesion score.
@@ -83,13 +86,14 @@ Deliveries are deduplicated on `(provider, external_id)` in the `ingest_events`
1. Add a slug to `ProviderSlug` in `backend/types/integration.ts`.
2. Add an entry to `providers` in `backend/config/providers.ts` with its signature scheme, header names, and OAuth endpoints.
3. Write one normalizer in `backend/config/normalizers.ts` mapping that provider's payload to notes.
4. Set the provider's signing secret and OAuth credentials in `backend/.env`.
4. Add an `enrich` hook only if the provider's payload references content it does not include.
5. Set the provider's signing secret and OAuth credentials in `backend/.env`.
## Development Roadmap
### Ingestion — pulling tagged artifacts in
- [ ] **Slack** — where decisions actually get made; a `:sticky:` emoji reaction fires an Events API webhook that pulls the message in.
- [x] **Slack** — where decisions actually get made; `/sticky` captures a new note, and an emoji reaction (`:pushpin:` by default) fires an Events API webhook that pulls an existing message in.
- [ ] **Microsoft Teams** — same capture gesture for enterprise shops; message extension plus Graph change notifications.
- [ ] **Jira** — label- or mention-triggered webhook scoped by JQL. (This is where comments typically carry half the backlog's context.)
- [ ] **Linear** — engineering-side tickets and threads; label-triggered GraphQL webhook.
@@ -155,6 +159,45 @@ EOF
The key must decode to exactly 32 bytes; the backend refuses to encrypt otherwise rather than falling back to something weaker.
## Slack integration
Two capture gestures, both landing in the same ingest pipeline:
- **`/sticky <your note>`** creates a note from what you type.
- **Reacting with an emoji** captures the message someone already wrote. The trigger defaults to `:pushpin:` and is set by `SLACK_CAPTURE_REACTION`.
### 1. Create the Slack app
At [api.slack.com/apps](https://api.slack.com/apps), create an app from scratch in your workspace. Under **OAuth & Permissions**, add the bot token scopes `channels:history`, `reactions:read`, `users:read`, and `commands`, then install the app to the workspace and copy the **Bot User OAuth Token** (`xoxb-…`). Under **Basic Information**, copy the **Signing Secret**.
```bash
cat >> backend/.env << 'EOF'
SLACK_BOT_TOKEN=xoxb-<bot user oauth token>
SLACK_CAPTURE_REACTION=pushpin
EOF
```
`SLACK_SIGNING_SECRET` is already in the integration variables above. There is no OAuth flow here on purpose: a single workspace reading credentials from the environment is far less machinery than an install dance, and the OAuth endpoints in the provider registry stay unused until multi-workspace support is actually needed.
### 2. Expose your local backend
Slack only delivers to a public HTTPS URL, so a tunnel is required for local development.
```bash
brew install ngrok
ngrok http 3001
```
Copy the `https://` forwarding URL ngrok prints. It changes every restart on the free plan, and both URLs below have to be updated when it does.
### 3. Point Slack at the tunnel
Under **Slash Commands**, create `/sticky` with the request URL `https://<your-ngrok-host>/v1/slack/commands`.
Under **Event Subscriptions**, enable events and set the request URL to `https://<your-ngrok-host>/v1/webhooks/slack`. Slack immediately sends a `url_verification` challenge, which the webhook route answers before signature checking, so the backend must already be running when you save. Subscribe to the bot event `reaction_added`.
Reinstall the app if Slack prompts you, then invite the bot to any channel you want to capture from with `/invite @<your app name>`. A reaction in a channel the bot is not a member of arrives as a `not_in_channel` error, which is classified as permanent and will not be retried.
### 3. Set up/run the database
Start DB for local development (assumes local dev env MacOS and Homebrew installed)