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

+273
View File
@@ -0,0 +1,273 @@
package sqlstore
import (
"context"
"database/sql"
"errors"
"fmt"
"github.com/dariuszjaniak/warp/server/internal/storage"
)
// DelayedEvents is the shared implementation of storage.DelayedEventStore.
type DelayedEvents struct {
db *sql.DB
rebind func(string) string
}
// NewDelayedEvents builds a delayed event store.
func NewDelayedEvents(db *sql.DB, rebind func(string) string) *DelayedEvents {
if rebind == nil {
rebind = func(query string) string { return query }
}
return &DelayedEvents{db: db, rebind: rebind}
}
func (s *DelayedEvents) q(query string) string { return s.rebind(query) }
const delayedEventColumns = `
delay_id, user_id, device_id, room_id, event_type, state_key, content,
delay_ms, running_since_ms, send_at_ms,
finalised_at_ms, outcome, reason, event_id, error`
const (
insertDelayedEventSQL = `
INSERT INTO delayed_events (
delay_id, user_id, device_id, room_id, event_type, state_key, content,
delay_ms, running_since_ms, send_at_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
selectDelayedEventSQL = `SELECT` + delayedEventColumns + `
FROM delayed_events WHERE user_id = ? AND delay_id = ?`
selectScheduledDelayedEventSQL = `SELECT` + delayedEventColumns + `
FROM delayed_events WHERE delay_id = ? AND finalised_at_ms = 0`
selectUserDelayedEventsSQL = `SELECT` + delayedEventColumns + `
FROM delayed_events WHERE user_id = ?
ORDER BY finalised_at_ms = 0 DESC, send_at_ms ASC, finalised_at_ms DESC`
countScheduledDelayedEventsSQL = `
SELECT COUNT(*) FROM delayed_events WHERE user_id = ? AND finalised_at_ms = 0`
restartDelayedEventSQL = `
UPDATE delayed_events SET running_since_ms = ?, send_at_ms = ? + delay_ms
WHERE delay_id = ? AND finalised_at_ms = 0`
finaliseDelayedEventSQL = `
UPDATE delayed_events
SET finalised_at_ms = ?, outcome = ?, reason = ?, event_id = ?, error = ?
WHERE delay_id = ? AND finalised_at_ms = 0`
selectDueDelayedEventsSQL = `SELECT` + delayedEventColumns + `
FROM delayed_events WHERE finalised_at_ms = 0 AND send_at_ms <= ?
ORDER BY send_at_ms LIMIT ?`
selectNextDelayedEventDueSQL = `
SELECT MIN(send_at_ms) FROM delayed_events WHERE finalised_at_ms = 0`
selectScheduledStateEventsSQL = `SELECT` + delayedEventColumns + `
FROM delayed_events
WHERE room_id = ? AND event_type = ? AND state_key = ? AND finalised_at_ms = 0`
purgeFinalisedDelayedEventsSQL = `
DELETE FROM delayed_events WHERE finalised_at_ms <> 0 AND finalised_at_ms < ?`
)
// InsertDelayedEvent implements storage.DelayedEventStore.
func (s *DelayedEvents) InsertDelayedEvent(ctx context.Context, e storage.DelayedEvent) error {
var stateKey sql.NullString
if e.StateKey != nil {
stateKey = sql.NullString{String: *e.StateKey, Valid: true}
}
if _, err := s.db.ExecContext(ctx, s.q(insertDelayedEventSQL),
e.DelayID, e.UserID, e.DeviceID, e.RoomID, e.Type, stateKey, string(e.Content),
e.DelayMS, e.RunningSinceMS, e.SendAtMS,
); err != nil {
return fmt.Errorf("insert delayed event: %w", err)
}
return nil
}
// DelayedEvent implements storage.DelayedEventStore.
func (s *DelayedEvents) DelayedEvent(ctx context.Context, userID, delayID string) (storage.DelayedEvent, error) {
return s.one(ctx, selectDelayedEventSQL, userID, delayID)
}
// ScheduledDelayedEvent implements storage.DelayedEventStore.
func (s *DelayedEvents) ScheduledDelayedEvent(ctx context.Context, delayID string) (storage.DelayedEvent, error) {
return s.one(ctx, selectScheduledDelayedEventSQL, delayID)
}
func (s *DelayedEvents) one(ctx context.Context, query string, args ...any) (storage.DelayedEvent, error) {
events, err := s.list(ctx, query, args...)
if err != nil {
return storage.DelayedEvent{}, err
}
if len(events) == 0 {
return storage.DelayedEvent{}, storage.ErrNoSuchDelayedEvent
}
return events[0], nil
}
// DelayedEvents implements storage.DelayedEventStore.
func (s *DelayedEvents) DelayedEvents(ctx context.Context, userID string) ([]storage.DelayedEvent, error) {
return s.list(ctx, selectUserDelayedEventsSQL, userID)
}
// CountScheduledDelayedEvents implements storage.DelayedEventStore.
func (s *DelayedEvents) CountScheduledDelayedEvents(ctx context.Context, userID string) (int, error) {
var n int
if err := s.db.QueryRowContext(ctx, s.q(countScheduledDelayedEventsSQL), userID).Scan(&n); err != nil {
return 0, fmt.Errorf("count delayed events: %w", err)
}
return n, nil
}
// RestartDelayedEvent implements storage.DelayedEventStore.
func (s *DelayedEvents) RestartDelayedEvent(ctx context.Context, delayID string, nowMS int64) error {
res, err := s.db.ExecContext(ctx, s.q(restartDelayedEventSQL), nowMS, nowMS, delayID)
if err != nil {
return fmt.Errorf("restart delayed event: %w", err)
}
return oneRow(res)
}
// FinaliseDelayedEvent implements storage.DelayedEventStore.
func (s *DelayedEvents) FinaliseDelayedEvent(ctx context.Context, e storage.DelayedEvent) error {
res, err := s.db.ExecContext(ctx, s.q(finaliseDelayedEventSQL),
e.FinalisedAtMS, e.Outcome, e.Reason, e.EventID, string(e.Error), e.DelayID)
if err != nil {
return fmt.Errorf("finalise delayed event: %w", err)
}
return oneRow(res)
}
func oneRow(res sql.Result) error {
n, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("delayed event: %w", err)
}
if n == 0 {
return storage.ErrNoSuchDelayedEvent
}
return nil
}
// DueDelayedEvents implements storage.DelayedEventStore.
func (s *DelayedEvents) DueDelayedEvents(ctx context.Context, nowMS int64, limit int) ([]storage.DelayedEvent, error) {
return s.list(ctx, selectDueDelayedEventsSQL, nowMS, limit)
}
// NextDelayedEventDue implements storage.DelayedEventStore.
func (s *DelayedEvents) NextDelayedEventDue(ctx context.Context) (int64, bool, error) {
var next sql.NullInt64
if err := s.db.QueryRowContext(ctx, s.q(selectNextDelayedEventDueSQL)).Scan(&next); err != nil {
return 0, false, fmt.Errorf("next delayed event: %w", err)
}
return next.Int64, next.Valid, nil
}
// ScheduledDelayedStateEvents implements storage.DelayedEventStore.
func (s *DelayedEvents) ScheduledDelayedStateEvents(ctx context.Context, roomID, eventType, stateKey string) ([]storage.DelayedEvent, error) {
return s.list(ctx, selectScheduledStateEventsSQL, roomID, eventType, stateKey)
}
// PurgeFinalisedDelayedEvents implements storage.DelayedEventStore.
func (s *DelayedEvents) PurgeFinalisedDelayedEvents(ctx context.Context, beforeMS int64) error {
if _, err := s.db.ExecContext(ctx, s.q(purgeFinalisedDelayedEventsSQL), beforeMS); err != nil {
return fmt.Errorf("purge delayed events: %w", err)
}
return nil
}
func (s *DelayedEvents) list(ctx context.Context, query string, args ...any) ([]storage.DelayedEvent, error) {
rows, err := s.db.QueryContext(ctx, s.q(query), args...)
if err != nil {
return nil, fmt.Errorf("read delayed events: %w", err)
}
defer rows.Close()
var out []storage.DelayedEvent
for rows.Next() {
var (
e storage.DelayedEvent
stateKey sql.NullString
content, errorContent string
)
if err := rows.Scan(&e.DelayID, &e.UserID, &e.DeviceID, &e.RoomID, &e.Type, &stateKey, &content,
&e.DelayMS, &e.RunningSinceMS, &e.SendAtMS,
&e.FinalisedAtMS, &e.Outcome, &e.Reason, &e.EventID, &errorContent,
); err != nil {
return nil, fmt.Errorf("read delayed event: %w", err)
}
if stateKey.Valid {
e.StateKey = &stateKey.String
}
e.Content = []byte(content)
if errorContent != "" {
e.Error = []byte(errorContent)
}
out = append(out, e)
}
return out, rows.Err()
}
// OpenIDTokens is the shared implementation of storage.OpenIDTokenStore.
type OpenIDTokens struct {
db *sql.DB
rebind func(string) string
}
// NewOpenIDTokens builds an OpenID token store.
func NewOpenIDTokens(db *sql.DB, rebind func(string) string) *OpenIDTokens {
if rebind == nil {
rebind = func(query string) string { return query }
}
return &OpenIDTokens{db: db, rebind: rebind}
}
const (
insertOpenIDTokenSQL = `
INSERT INTO openid_tokens (token_hash, user_id, expires_at_ms) VALUES (?, ?, ?)`
selectOpenIDTokenSQL = `
SELECT user_id FROM openid_tokens WHERE token_hash = ? AND expires_at_ms > ?`
purgeOpenIDTokensSQL = `DELETE FROM openid_tokens WHERE expires_at_ms < ?`
)
// InsertOpenIDToken implements storage.OpenIDTokenStore.
func (s *OpenIDTokens) InsertOpenIDToken(ctx context.Context, tokenHash, userID string, expiresAtMS int64) error {
if _, err := s.db.ExecContext(ctx, s.rebind(insertOpenIDTokenSQL), tokenHash, userID, expiresAtMS); err != nil {
return fmt.Errorf("insert OpenID token: %w", err)
}
return nil
}
// OpenIDTokenUser implements storage.OpenIDTokenStore.
func (s *OpenIDTokens) OpenIDTokenUser(ctx context.Context, tokenHash string, nowMS int64) (string, error) {
var userID string
err := s.db.QueryRowContext(ctx, s.rebind(selectOpenIDTokenSQL), tokenHash, nowMS).Scan(&userID)
if errors.Is(err, sql.ErrNoRows) {
return "", storage.ErrNoSuchOpenIDToken
}
if err != nil {
return "", fmt.Errorf("read OpenID token: %w", err)
}
return userID, nil
}
// PurgeOpenIDTokens implements storage.OpenIDTokenStore.
func (s *OpenIDTokens) PurgeOpenIDTokens(ctx context.Context, beforeMS int64) error {
if _, err := s.db.ExecContext(ctx, s.rebind(purgeOpenIDTokensSQL), beforeMS); err != nil {
return fmt.Errorf("purge OpenID tokens: %w", err)
}
return nil
}
// Compile-time proof that the shared implementations satisfy the interfaces.
var (
_ storage.DelayedEventStore = (*DelayedEvents)(nil)
_ storage.OpenIDTokenStore = (*OpenIDTokens)(nil)
)
@@ -69,6 +69,8 @@ type Store struct {
push *Push
admin *Admin
apps *AppServices
delayed *DelayedEvents
openID *OpenIDTokens
}
// New builds a Store. It does not touch the database.
@@ -93,6 +95,8 @@ func New(opts Options) (*Store, error) {
push: NewPush(opts.DB, opts.Rebind),
admin: NewAdmin(opts.DB, opts.Rebind, opts.IsUniqueViolation),
apps: NewAppServices(opts.DB, opts.Rebind),
delayed: NewDelayedEvents(opts.DB, opts.Rebind),
openID: NewOpenIDTokens(opts.DB, opts.Rebind),
}, nil
}
@@ -138,6 +142,12 @@ func (s *Store) Admin() storage.AdminStore { return s.admin }
// AppServices implements storage.Database.
func (s *Store) AppServices() storage.AppServiceStore { return s.apps }
// DelayedEvents implements storage.Database.
func (s *Store) DelayedEvents() storage.DelayedEventStore { return s.delayed }
// OpenIDTokens implements storage.Database.
func (s *Store) OpenIDTokens() storage.OpenIDTokenStore { return s.openID }
// Migrate implements storage.Database.
func (s *Store) Migrate(ctx context.Context) error {
ms, err := s.migrations()