Group voice and video calls with MatrixRTC and LiveKit
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s

Every call in the web client now goes through MatrixRTC with a LiveKit
SFU, the model Element Call and Element X use; the legacy 1:1 m.call.*
calls are gone. In a room of two the other person is rung and can answer
or decline; in a bigger room a banner offers to join the call. Calls show
a grid of participant tiles with mute, camera, screen sharing and leave,
and media is end-to-end encrypted in encrypted rooms.

Server:
- MSC4140 delayed events (send/state with a delay, plus the
  delayed_events list/restart/cancel/send API), stored and scheduled, so
  a member whose client drops off is taken out of the call.
- POST /user/{userId}/openid/request_token, which lk-jwt-service uses to
  hand out LiveKit tokens.
- org.matrix.msc4143.rtc_foci in /.well-known/matrix/client, configured
  under client_api.rtc.
- Room presets let members send call membership state.
- Migration 0014 for both SQL dialects.

Deploy: LiveKit and lk-jwt-service in compose (calls.yml) and the helm
chart. Docs: docs/calls.md, configuration and deployment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5.5 committed 2026-10-18 19:33:29 +02:00
1 parent 325431a5be
commit 76dfc98a86
49 files changed
+3849 -432

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
package clientapi_test
import (
"net/http"
"net/url"
"testing"
"github.com/dariuszjaniak/warp/server/internal/httputil"
)
// TestOpenIDToken is the exchange the LiveKit JWT service relies on: a user
// asks for a token, hands it over, and the service asks this server who it
// belongs to.
func TestOpenIDToken(t *testing.T) {
ts := newTestServer(t, nil)
alice := ts.register("alice", "correct-horse-battery-staple")
bob := ts.register("bob", "correct-horse-battery-staple")
path := "/_matrix/client/v3/user/" + url.PathEscape(alice.UserID) + "/openid/request_token"
rec := ts.post(path, alice.AccessToken, map[string]any{})
expectStatus(t, rec, http.StatusOK)
body := jsonBody(t, rec)
token, _ := body["access_token"].(string)
if token == "" || body["token_type"] != "Bearer" || body["matrix_server_name"] != testServerName ||
body["expires_in"] != float64(3600) {
t.Fatalf("request_token = %s", rec.Body.String())
}
// The token is no access token.
expectError(t, ts.get("/_matrix/client/v3/account/whoami", token), http.StatusUnauthorized, httputil.ErrCodeUnknownToken)
rec = ts.get("/_matrix/federation/v1/openid/userinfo?access_token="+url.QueryEscape(token), "")
expectStatus(t, rec, http.StatusOK)
if got := jsonBody(t, rec)["sub"]; got != alice.UserID {
t.Errorf("sub = %v, want %s", got, alice.UserID)
}
expectError(t, ts.get("/_matrix/federation/v1/openid/userinfo?access_token=nope", ""),
http.StatusUnauthorized, httputil.ErrCodeUnknownToken)
expectError(t, ts.get("/_matrix/federation/v1/openid/userinfo", ""),
http.StatusUnauthorized, "M_MISSING_TOKEN")
// Nobody gets a token in someone else's name.
expectError(t, ts.post(path, bob.AccessToken, map[string]any{}), http.StatusForbidden, httputil.ErrCodeForbidden)
}
// TestOpenIDUserInfoIgnoresFederationSwitch: the userinfo endpoint answers
// with federation off, so a server that does not federate can still let its
// users into group calls.
func TestOpenIDUserInfoIgnoresFederationSwitch(t *testing.T) {
ts := newTestServer(t, nil)
if ts.settings.FederationEnabled() {
t.Fatal("the test server is expected to start with federation off")
}
alice := ts.register("alice", "correct-horse-battery-staple")
rec := ts.post("/_matrix/client/v3/user/"+url.PathEscape(alice.UserID)+"/openid/request_token",
alice.AccessToken, nil)
expectStatus(t, rec, http.StatusOK)
token, _ := jsonBody(t, rec)["access_token"].(string)
expectStatus(t, ts.get("/_matrix/federation/v1/openid/userinfo?access_token="+url.QueryEscape(token), ""), http.StatusOK)
}