jid

arizukoreference › jid

Every chat and every sender is a JID: a typed URI of the form <platform>:<rest>. The first segment of <rest> is a kind discriminator. Built on net/url for free RFC 3986 compliance + percent-encoding; matched with path.Match glob semantics across all routing keys.

1. Wire form & grammar

From core/jid.go:28 (ParseJID):

jid     := platform ":" rest
platform := scheme (lowercase adapter name; JID.Platform())
rest    := non-empty opaque path (JID.Path())
kind    := first segment of rest before "/" (JID.Kind())

Validation:

2. Code types

From core/jid.go:18:

type JID struct{ u *url.URL }   // wire-typed base

type ChatJID struct{ JID }       // a destination/chat resource
type UserJID struct{ JID }       // a user identity resource

Message.ChatJID is a typed ChatJID string; Message.Sender is a typed UserJID string. The Go compiler refuses to swap them. Both implement database/sql.Scanner and driver.Valuer, and both round-trip JSON (MarshalJSONUnmarshalJSON). The empty wire string scans to a zero JID and emits "".

3. Per-platform schemas

Each adapter owns its <rest> shape. From specs/5/S-jid-format.md § "Per-platform schemas" plus the rewriter rules in 0042-typed-jids.sql:

platformkindshapeexample
telegramusertelegram:user/<chat_id>telegram:user/12345
grouptelegram:group/<chat_id>telegram:group/67890
discord<guild_id>discord:<guild_id>/<channel_id>discord:5678/9012
dmdiscord:dm/<channel_id>discord:dm/54321
userdiscord:user/<user_id>discord:user/u123
whatsapp— (server suffix)whatsapp:<id>@<server>whatsapp:1234@g.us, …@s.whatsapp.net, …@lid
mastodonaccountmastodon:account/<account_id>mastodon:account/42
statusmastodon:status/<status_id>mastodon:status/abc
redditcommentreddit:comment/<id>reddit:comment/abc
submissionreddit:submission/<id>reddit:submission/xyz
dmreddit:dm/<id>reddit:dm/m123
userreddit:user/<username>reddit:user/alice
blueskyuserbluesky:user/<did> (DID's : percent-encoded)bluesky:user/did%3Aplc%3A123
postbluesky:post/<at_uri>bluesky:post/at%3A…
twitterusertwitter:user/<user_id>twitter:user/123
tweettwitter:tweet/<tweet_id>twitter:tweet/456
dmtwitter:dm/<id>twitter:dm/789
linkedinuserlinkedin:user/<urn>linkedin:user/urn123
postlinkedin:post/<urn>linkedin:post/urn456
emailaddressemail:address/<addr>email:address/foo@bar.com
threademail:thread/<msgid>email:thread/<Message-ID>
webfolderweb:<folder>[/<suffix>] (spec)web:acme/support
userweb:user/<sub>web:user/sub-1
hookhook:<folder>/<source>[/<suffix>] (spec)hook:acme/eng/github
slackper adapter; slack: JIDs handled by slakd

Discord placeholder. Legacy rows with no stored guild_id migrated to discord:_/<channel> (placeholder kind). New inbound from discd carries the real guild ID. See 0042-typed-jids.sql:107.

Web is folder-keyed. The chat JID is the folder (with optional /<suffix>); the URL token is a separate row in route_tokens that maps to this JID. See route tokens and spec 5/W.

4. Routing & glob matching

The route table's match column is a space-separated list of key=glob predicates. Evaluation lives in router.RouteMatches; keys are extracted from the message via msgField:

keyextractssource
platformJidPlatform(msg.ChatJID) — everything before the first :core/types.go:183
roomJidRoom(msg.ChatJID) — everything after the first :core/types.go:77
chat_jidmsg.ChatJID — full canonical string
sendermsg.Sender — full sender JID
verbmsg.Verb"message" default; "join", "edit", …

Glob match uses Go's path.Match semantics:

From the RouteMatches contract (router.go:290-324):

predicatematches when
key=<exact>value equals <exact>
key=<glob>value matches glob (*, ?, […]; * doesn't cross /)
key=*value is present (non-empty) — bare * silently rejects empty
key=value is absent (empty)
omit keyunconstrained

Malformed tokens (no = or empty key) are skipped, not errored. The empty match expression matches everything.

The same path.Match-based matcher is available as core.MatchJID(pattern, value) for direct JID-pattern checks outside the route engine.

5. Examples

# all telegram groups
match='platform=telegram chat_jid=telegram:group/*'

# discord guild 67890, any channel/thread
match='chat_jid=discord:67890/*'

# all Discord DMs
match='chat_jid=discord:dm/*'

# all WhatsApp groups (server suffix discriminates)
match='chat_jid=whatsapp:*@g.us'

# all activity on a Mastodon instance (single account-id digit-or-letter)
match='chat_jid=mastodon:account/*'

# specific Telegram room by signed numeric ID (room=<rest after colon>)
match='platform=telegram room=group/-100123456'

# any non-message verb on any platform (join, edit, delete, …)
match='verb=*'
match='verb=' is the negation: only verb-less (default 'message') rows

6. Globs in grant rules

The same jid=<glob> syntax appears in grant rules (grants reference). The matcher there is grants.matchGlob with isValueDelim stopping * at , or ) — not at /. That's a deliberate difference: grant param globs span the whole JID, route globs are segment-aware.

send(jid=telegram:group/*)     # rule: only telegram groups (grants matcher: * spans /)
match='chat_jid=telegram:group/*'  # route: only telegram groups (path.Match: * doesn't cross /)

For top-level platform globs both forms agree (telegram:*/* in routes vs telegram:* in grants); both mean "anything telegram." For grant params there's only one trailing segment to match against, so the wider * is harmless.

7. Hard cutover & migration history

Migrations 0042-typed-jids.sql and 0043-typed-jids-tail.sql rewrote every JID-shaped value in the store to the typed form. Affected columns:

Every UPDATE is guarded by a NOT LIKE on the new shape, so re-running is idempotent. Rules:

8. Design discipline

9. Go deeper