add NIP-29 LiveKit token and fork detection
This commit is contained in:
+83
-16
@@ -490,18 +490,61 @@ group's relay only, and treat the relay's `OK` as the truth.
|
||||
"Absent" and "empty" are deliberately the same value here: unlike
|
||||
`supported_kinds`, the NIP gives the empty list no special meaning, so the
|
||||
UI reads "no one is live" either way.
|
||||
2. The `/.well-known/nip29/livekit/<group-id>` token endpoint: a NIP-98
|
||||
`27235` event in `Authorization`, returning the LiveKit JWT and server URL,
|
||||
plus the `204` probe at `/.well-known/nip29/livekit` that tells the UI
|
||||
whether a relay supports AV at all. Not started. This is the only part that
|
||||
needs an HTTP client in the crate, so it is where the dependency decision
|
||||
(if any) lands; `livekit` on `39000` is already parsed but currently inert.
|
||||
3. Fork/migration detection: read the `10009` of the group's admins (pubkeys
|
||||
from `39001`, cached locally) and compare the relay hint against the relay
|
||||
we are talking to; a different hint means the group may have moved or been
|
||||
forked, which is the client's cue to offer fetching from the new relay.
|
||||
Not started; the periodic — and MUST-when-offline — trigger is what
|
||||
`Monitor` would feed, so the two decisions go together.
|
||||
2. The `/.well-known/nip29/livekit/<group-id>` token endpoint and the `204`
|
||||
probe at `/.well-known/nip29/livekit`. **Done.** The HTTP client is the one
|
||||
GPUI already owns: `cx.http_client()` returning `Arc<dyn HttpClient>`,
|
||||
already wired to `reqwest_client::ReqwestClient` on desktop and to `fetch`
|
||||
on wasm — the same path `state::nip05` uses, which is why no HTTP
|
||||
dependency lands in this crate. Its `get()` convenience cannot set headers,
|
||||
so the request goes through `HttpClient::send` with a `Builder`, an
|
||||
explicit `RedirectPolicy::NoFollow` (the `Authorization` header is signed
|
||||
for exactly one URL, so a redirect could only ever fail verification) and a
|
||||
10s `timeout` — both are opt-in extensions, and a request without them has
|
||||
none. The header itself is NIP-98 from the SDK: `nostr/nip98` is now
|
||||
enabled on the workspace `nostr` entry and nip29 declares `nostr` itself,
|
||||
so `HttpData::new(url, HttpMethod::GET).to_authorization(&signer)` builds
|
||||
`Nostr <base64>` — the `u` + `method` tags that the crate's own
|
||||
`verify_auth_header` demands, though the NIP's prose mentions only `u`.
|
||||
`GroupKey::livekit_token_url` / `livekit_probe_url` derive the endpoint
|
||||
purely: the relay URL is rewritten to http(s) (keeping host and port),
|
||||
reset to the origin, and the path built with `pop_if_empty().push(..)` so
|
||||
each segment is escaped — a relay with its own path prefix loses it, since
|
||||
the NIP puts the endpoint at the absolute path. `Group::livekit_session`
|
||||
and `Group::livekit_supported` are the two calls, both `&App` +
|
||||
`background_spawn` like `publish`. The session is returned, not stored:
|
||||
tokens are per-join artifacts, not group state, so the caller owns it.
|
||||
Only `livekit_supported`'s unauthenticated GET ignores status beyond
|
||||
`204`; the token endpoint's response shape is **not specified by the NIP**
|
||||
(see Decisions) and no LiveKit client library is involved — the backend
|
||||
stops at the JWT and server URL.
|
||||
3. Fork/migration detection. **Done.** One standing `nip29-forks`
|
||||
subscription over `{authors: <every admin of every group>, kinds: [10009]}`
|
||||
on `ReqTarget::auto` — the admins' own group lists live on their outbox
|
||||
relays, so this works with the group's relay down, and the admin pubkeys
|
||||
come from `39001`, which is hydrated from the envelope cache, so it works
|
||||
after a restart too. Each event is folded into `forks` (author → newest
|
||||
`GroupList`, guarded on `created_at` because `10009` is replaceable), then
|
||||
`evaluate_forks` recomputes every group. `Group::elsewhere()` is `Some`
|
||||
when an admin of that group lists the same group id at a different relay;
|
||||
the data cannot distinguish a move from a fork, hence the neutral name.
|
||||
Evaluation is incremental, on each event, rather than batched on EOSE:
|
||||
`ReqTarget::auto` fans out to several relays, so the first EOSE would
|
||||
otherwise evaluate — and clear — against one relay's partial view.
|
||||
Nothing is auto-joined, since the NIP asks the client to *offer*; the
|
||||
offer is `GroupsRegistry::join_key(elsewhere, None, cx)`, which is exactly
|
||||
"fetch from the new relay and update our own `10009`". Two triggers,
|
||||
because the NIP's "periodically" and its "MUST when the relay is offline"
|
||||
are different things. Reactively, the group observer calls
|
||||
`sync_forks(false)` on `GroupEvent::Updated`, subscribing as soon as the
|
||||
first `39001` lands and re-subscribing only when the admin set actually
|
||||
changes — the guard matters, since `Updated` also fires for metadata,
|
||||
pins, membership and a finished page. Periodically, a
|
||||
`background_executor().timer` loop started in `load` pushes
|
||||
`Signal::Forks` every 15 minutes and `sync_forks(true)` re-queries
|
||||
regardless: that is the recovery path for a subscription that died or a
|
||||
client that was offline, not the detection path. `reset` drops the fork
|
||||
state and unsubscribes the REQ; a `CLOSED` on it is only logged, because
|
||||
the tick retries and there is nothing to repair.
|
||||
|
||||
## UI later — the hooks this backend leaves
|
||||
|
||||
@@ -514,6 +557,9 @@ group's relay only, and treat the relay's `OK` as the truth.
|
||||
| Send / react | `Group::send_message` / `send_reaction` |
|
||||
| Members, admins, roles, flags | `Group::members/admins/roles/metadata` |
|
||||
| Who is in the AV room | `Group::participants()` |
|
||||
| Join AV (get a LiveKit session) | `Group::livekit_session(cx)` → `await` → `LivekitSession { token, server_url }` |
|
||||
| Is AV available here | `Group::livekit_supported(cx)` after seeing `metadata().livekit` |
|
||||
| The group may have moved or been forked | `Group::elsewhere()` → offer `GroupsRegistry::join_key(elsewhere, None, cx)` |
|
||||
| Membership badge (member, pending, refused, removed) | `Group::membership()` |
|
||||
| Copy an invite link | `GroupMetadata::naddr(relay, invite)` |
|
||||
| Leave | `GroupsRegistry::leave` |
|
||||
@@ -533,11 +579,14 @@ join dialog accepts a pasted naddr (with or without `?invite=`). System
|
||||
notifications can hang off `GroupEvent::Incoming` when the window is inactive.
|
||||
|
||||
Nothing new is needed from GPUI to support this backend. The local checkouts
|
||||
are `gpui-pre` 0.3.5 (aliased as `gpui`) and `gpui-base` 0.7.0, which `crates/ui`
|
||||
styles; the only GPUI APIs the backend uses are the ones `chat` and `community`
|
||||
are `gpui-pre` 0.3.7 (aliased as `gpui`) and `gpui-base` 0.7.0, which `crates/ui`
|
||||
styles; the GPUI APIs the backend uses are the ones `chat` and `community`
|
||||
already use — `Entity`/`WeakEntity`, `Context::spawn`, `background_spawn`,
|
||||
`EventEmitter`, `Subscription` — and the panel's components (`ListState`,
|
||||
`MessageRow`, `Avatar`, `Button`, `Scrollbar`) already exist in the app.
|
||||
`EventEmitter`, `Subscription` — plus two that `state` already uses:
|
||||
`App::http_client()` for the AV token fetch and
|
||||
`App::background_executor().timer(..)` for the fork tick. The panel's components
|
||||
(`ListState`, `MessageRow`, `Avatar`, `Button`, `Scrollbar`) already exist in the
|
||||
app.
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -581,3 +630,21 @@ already use — `Entity`/`WeakEntity`, `Context::spawn`, `background_spawn`,
|
||||
handles the reads.
|
||||
8. **References.** `naddr` only (`?invite=` accepted); `id@relay` is not. A group
|
||||
whose relay publishes no `39000` cannot be referenced or joined in v1.
|
||||
9. **LiveKit response shape.** The NIP says the token endpoint returns "the
|
||||
LiveKit JWT and the URL of the LiveKit server" but names no fields, so
|
||||
`LivekitSession::parse` reads `token` (or `jwt`) and `url` (or `server_url`)
|
||||
and fails loudly on anything else. This is a guess, not a spec, and it is the
|
||||
one part of the AV path that a real relay can contradict; it is worth
|
||||
re-checking against whichever relay is targeted before the UI lands.
|
||||
Likewise `livekit_supported` treats "not `204`" as "no AV" rather than an
|
||||
error, because it only feeds a button.
|
||||
10. **`Monitor`.** Not adopted. It is only reachable through
|
||||
`ClientBuilder::monitor(Monitor::new(n))`, so nothing can read per-relay
|
||||
status until `crates/state` opts in, and its `StatusChanged` stream does not
|
||||
distinguish a permanent outage from a reconnect — so it would need filtering
|
||||
before it could mean anything. The only thing it buys here is running the
|
||||
admins' `10009` check the instant a group's relay drops, rather than at the
|
||||
next 15-minute tick. The NIP frames that as MUST, but the substance is
|
||||
already covered: the admins come from the cached `39001`, so the check runs
|
||||
with the relay gone, just later. If that latency ever matters, the hook is
|
||||
`sync_forks(true, cx)` driven by a `StatusChanged` consumer — one place.
|
||||
|
||||
Reference in New Issue
Block a user