Skip to content

Discord

Discord capture has two independent halves: a live bot that polls channels going forward, and a backfill that imports Discord's own data export for history before the bot was turned on. You can use either on its own, or both together.

Live bot (kind=discord)

What it does

A bot polls GET /channels/{id}/messages for each allowlisted channel, paging forward from a per-channel Discord snowflake cursor (like the IMAP connector's UID cursor, but one cursor per channel instead of per folder). New messages are normalized the same way as everything else and written to cap_items.

No privileged Gateway intent required

Discord's privileged Message Content intent only gates the Gateway push-event stream. This connector never opens a Gateway connection — it reads history over the REST API, which doesn't need that intent. The bot only needs the ordinary View Channel and Read Message History permissions on the channels it should poll.

Prereqs

  • A Discord bot application with a bot token, invited to the server(s) you want to capture, with View Channel + Read Message History permission on each channel you'll list in config.channels.
  • Runs in the container scheduler — no host access needed.
  • DMs are out of scope: a bot cannot enumerate a user's DM channels, so only guild channels can be polled live.

Env / secret_ref

The bot token is a single secret key, TOKEN. This page uses secret_ref DISCORD, giving the env var declared in module.yaml:

JARVIS_DISCORD_TOKEN=your-bot-token

Set it in .env. (You can pick a different secret_ref if you want a different env var name — see Overview — but DISCORD matches what the capture module already declares.)

Not yet wired into compose.yaml

Unlike the IMAP env vars, the scheduler service's environment: block in compose.yaml doesn't currently forward a JARVIS_*_TOKEN var through to the container. Until that's added, a token set only in .env won't reach the scheduler process — the poll will report missing credentials even with the env var set. Check compose.yaml's scheduler.environment for this connector before relying on the live bot.

The cap_connectors row

{
  "kind": "discord",
  "enabled": true,
  "secret_ref": "DISCORD",
  "config": {
    "guild_id": "your-guild-id",
    "channels": ["channel-id-1", "channel-id-2"]
  },
  "owner_addresses": ["your-discord-user-id"]
}

config.channels is an explicit allowlist — only the channel ids listed are polled. guild_id is contextual only (for your own reference); it isn't used to auto-discover channels. owner_addresses should be your own Discord user id, so your own messages are tagged direction="out".

Turn it on

  1. Set JARVIS_DISCORD_TOKEN in .env (see the compose.yaml caveat above).
  2. Restart the stack: ./jarvis down then ./jarvis up.
  3. Create the cap_connectors row above via the PocketBase admin UI.

The scheduler's capture_agent.poll cron (every 10 minutes) picks up the row on its next run. The cursor it writes back maps each channel id to the highest message snowflake seen so far, e.g. {"channel-id-1": "1234567890123456"}.

Verify

./jarvis logs scheduler

Then, in the PocketBase admin: cap_items grows with new Discord messages, the row's last_poll_at updates each cron run, and last_error is empty.

Errors

  • Missing credentials — same as every connector: last_error becomes missing credentials (set the secret_ref env vars) and the row is skipped until the token is set and reachable.
  • Per-channel rate limiting — a channel that gets rate-limited (HTTP 429) is retried a bounded number of times; if the limit doesn't clear in time, that channel is skipped for this run with its cursor left unadvanced (other channels in the same row still poll normally), and it's retried on the next cron run.

Backfill (data export)

What it does

A one-shot import of Discord's own "Request all my Data" export package. The export only ever contains messages you sent, so every imported item is tagged direction="out" and resolves to a single owner entity — there's no cap_connectors row involved; it's a CLI command you run once (or re-run with a later --since date).

Prereqs

  • Request your data export from Discord (User Settings → Privacy & Safety → Request all of my Data) and unzip the package locally.
  • Optionally, your Discord user id, if the export package doesn't include account/user.json (some trimmed exports omit it).

Run it

python -m capture_agent.backfill discord --package /path/to/discord-export --since 2026-01-01

If the package has no account/user.json, pass your id explicitly:

python -m capture_agent.backfill discord --package /path/to/discord-export --since 2026-01-01 --owner your-discord-user-id

--since filters to messages on or after that date (YYYY-MM-DD).

Verify

The command prints a summary when it finishes, e.g. capture discord backfill: {'ingested': N, 'skipped_dupe': 0, 'unrouted': N, 'entities_created': 1, 'errors': 0}. Check cap_items in the PocketBase admin for the new rows.

Errors

Per-message parsing failures are logged and skipped so one bad row never aborts the whole import — re-running the same command later is safe, since cap_items dedupes by external_id and already-imported messages are left alone.