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¶
- Set
JARVIS_DISCORD_TOKENin.env(see the compose.yaml caveat above). - Restart the stack:
./jarvis downthen./jarvis up. - Create the
cap_connectorsrow 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_errorbecomesmissing 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.