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>
274 lines
9.4 KiB
Go
274 lines
9.4 KiB
Go
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)
|
|
)
|