Files
Warp/server/internal/clientapi/delayed.go
T
tajniak81andClaude Opus 5.5 76dfc98a86
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
Group voice and video calls with MatrixRTC and LiveKit
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>
2026-10-18 19:33:29 +02:00

260 lines
8.9 KiB
Go

package clientapi
import (
"encoding/json"
"errors"
"net/http"
"slices"
"strconv"
"time"
"github.com/dariuszjaniak/warp/server/internal/delayedevents"
"github.com/dariuszjaniak/warp/server/internal/httputil"
"github.com/dariuszjaniak/warp/server/internal/roomserver"
"github.com/dariuszjaniak/warp/server/internal/storage"
"github.com/dariuszjaniak/warp/server/internal/userapi"
)
// MSC4140 is still unstable, so its endpoints and parameters carry its
// prefix.
const (
delayedEventsPrefix = "/_matrix/client/unstable/org.matrix.msc4140"
delayParam = "org.matrix.msc4140.delay"
parentDelayParam = "org.matrix.msc4140.parent_delay_id"
)
// registerDelayedEventRoutes adds the MSC4140 endpoints, if delayed events
// are switched on.
//
// The action endpoints are public: the delay ID is the capability, which is
// what lets a client hand its delayed leave to something else to keep alive.
// Both spellings are served — the action in the path, which is the current
// draft, and in the body, which clients fall back to.
func (c *ClientAPI) registerDelayedEventRoutes(r *httputil.Router) {
if !c.delayed.Enabled() || c.rooms == nil {
return
}
c.authed(r, "GET "+delayedEventsPrefix+"/delayed_events", c.limitDefault, c.handleGetDelayedEvents)
c.authed(r, "GET "+delayedEventsPrefix+"/delayed_events/{delayID}", c.limitDefault, c.handleGetDelayedEvent)
c.public(r, "POST "+delayedEventsPrefix+"/delayed_events/{delayID}", c.limitDefault, c.handleUpdateDelayedEvent)
c.public(r, "POST "+delayedEventsPrefix+"/delayed_events/{delayID}/{action}", c.limitDefault, c.handleUpdateDelayedEvent)
}
// delayRequested reads the delay parameter of a send or state PUT. ok is
// false when there is none, and the event is sent at once as usual.
func (c *ClientAPI) delayRequested(r *http.Request) (time.Duration, bool, error) {
q := r.URL.Query()
if q.Has(parentDelayParam) {
return 0, false, httputil.InvalidParam("%s is not supported", parentDelayParam)
}
if !q.Has(delayParam) {
return 0, false, nil
}
if !c.delayed.Enabled() {
// Refused rather than ignored: sending a delayed "left the call"
// at once would be exactly the wrong thing.
return 0, false, httputil.InvalidParam("Delayed events are not enabled on this server")
}
ms, err := strconv.ParseInt(q.Get(delayParam), 10, 64)
if err != nil || ms < 0 {
return 0, false, httputil.InvalidParam("%s must be a non-negative number of milliseconds", delayParam)
}
return time.Duration(ms) * time.Millisecond, true, nil
}
// delayIDResponse answers a delayed send.
type delayIDResponse struct {
DelayID string `json:"delay_id"`
}
// scheduleDelayed schedules what a send or state PUT asked to delay.
func (c *ClientAPI) scheduleDelayed(w http.ResponseWriter, r *http.Request, req delayedevents.ScheduleRequest) error {
delayID, err := c.delayed.Schedule(r.Context(), req)
if err != nil {
return delayedError(err, c.delayed.MaxDelay())
}
httputil.WriteJSON(w, http.StatusOK, delayIDResponse{DelayID: delayID})
return nil
}
// delayedError maps the service's errors onto Matrix errors. An overlong
// delay is reported the way Synapse does: M_UNKNOWN for a client that knows
// nothing of MSC4140, with the MSC's own code and the maximum alongside.
func delayedError(err error, maxDelay time.Duration) error {
switch {
case errors.Is(err, delayedevents.ErrMaxDelayExceeded):
merr := httputil.NewError(http.StatusBadRequest, httputil.ErrCodeUnknown, "%s", err.Error())
merr.DelayedErrCode = "M_MAX_DELAY_EXCEEDED"
merr.MaxDelayMS = maxDelay.Milliseconds()
return merr
case errors.Is(err, delayedevents.ErrTooMany):
return httputil.NewError(http.StatusBadRequest, "M_LIMIT_EXCEEDED", "%s", err.Error())
case errors.Is(err, delayedevents.ErrDisabled):
return httputil.Unrecognized()
case errors.Is(err, storage.ErrNoSuchDelayedEvent):
return httputil.NotFound("No such scheduled delayed event")
default:
return roomError(err)
}
}
// delayedEventJSON is one delayed event as MSC4140 lists it.
type delayedEventJSON struct {
DelayID string `json:"delay_id"`
RoomID string `json:"room_id"`
Type string `json:"type"`
StateKey *string `json:"state_key,omitempty"`
Content json.RawMessage `json:"content"`
Delay int64 `json:"delay"`
RunningSince int64 `json:"running_since"`
}
type finalisedDelayedEventJSON struct {
DelayedEvent delayedEventJSON `json:"delayed_event"`
Outcome string `json:"outcome"`
Reason string `json:"reason"`
Error json.RawMessage `json:"error,omitempty"`
EventID string `json:"event_id,omitempty"`
OriginServerTS int64 `json:"origin_server_ts,omitempty"`
}
type delayedEventsResponse struct {
Scheduled []delayedEventJSON `json:"scheduled"`
Finalised []finalisedDelayedEventJSON `json:"finalised"`
// DelayedEvents is the scheduled list under the name an earlier draft
// of MSC4140 used.
DelayedEvents []delayedEventJSON `json:"delayed_events"`
}
func toDelayedEventJSON(e storage.DelayedEvent) delayedEventJSON {
return delayedEventJSON{
DelayID: e.DelayID,
RoomID: e.RoomID,
Type: e.Type,
StateKey: e.StateKey,
Content: e.Content,
Delay: e.DelayMS,
RunningSince: e.RunningSinceMS,
}
}
func toFinalisedJSON(e storage.DelayedEvent) finalisedDelayedEventJSON {
out := finalisedDelayedEventJSON{
DelayedEvent: toDelayedEventJSON(e),
Outcome: e.Outcome,
Reason: e.Reason,
Error: e.Error,
EventID: e.EventID,
}
if e.EventID != "" {
out.OriginServerTS = e.FinalisedAtMS
}
return out
}
// handleGetDelayedEvents implements
// GET /_matrix/client/unstable/org.matrix.msc4140/delayed_events.
//
// status narrows the answer to scheduled or finalised events, and delay_id,
// which may repeat, to particular ones. Nobody has enough delayed events
// for paging to matter, so there is never a next_batch.
func (c *ClientAPI) handleGetDelayedEvents(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error {
q := r.URL.Query()
status := q.Get("status")
if status != "" && status != "scheduled" && status != "finalised" {
return httputil.InvalidParam("status must be scheduled or finalised")
}
ids := q["delay_id"]
events, err := c.delayed.List(r.Context(), auth.UserID)
if err != nil {
return err
}
resp := delayedEventsResponse{
Scheduled: []delayedEventJSON{},
Finalised: []finalisedDelayedEventJSON{},
}
for _, e := range events {
if len(ids) > 0 && !slices.Contains(ids, e.DelayID) {
continue
}
if e.FinalisedAtMS == 0 {
if status != "finalised" {
resp.Scheduled = append(resp.Scheduled, toDelayedEventJSON(e))
}
} else if status != "scheduled" {
resp.Finalised = append(resp.Finalised, toFinalisedJSON(e))
}
}
resp.DelayedEvents = resp.Scheduled
httputil.WriteJSON(w, http.StatusOK, resp)
return nil
}
// handleGetDelayedEvent implements
// GET /_matrix/client/unstable/org.matrix.msc4140/delayed_events/{delayID}.
func (c *ClientAPI) handleGetDelayedEvent(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error {
e, err := c.delayed.Get(r.Context(), auth.UserID, r.PathValue("delayID"))
if err != nil {
return delayedError(err, 0)
}
if e.FinalisedAtMS == 0 {
httputil.WriteJSON(w, http.StatusOK, toDelayedEventJSON(e))
} else {
httputil.WriteJSON(w, http.StatusOK, toFinalisedJSON(e))
}
return nil
}
type updateDelayedEventRequest struct {
Action string `json:"action"`
}
// handleUpdateDelayedEvent implements
// POST /_matrix/client/unstable/org.matrix.msc4140/delayed_events/{delayID}[/{action}].
func (c *ClientAPI) handleUpdateDelayedEvent(w http.ResponseWriter, r *http.Request) error {
delayID := r.PathValue("delayID")
action := r.PathValue("action")
if action == "" {
var req updateDelayedEventRequest
if err := httputil.DecodeJSON(w, r, &req); err != nil {
return err
}
action = req.Action
}
var err error
switch action {
case "restart":
err = c.delayed.Restart(r.Context(), delayID)
case "cancel":
err = c.delayed.Cancel(r.Context(), delayID)
case "send":
err = c.delayed.Send(r.Context(), delayID)
case "":
return httputil.MissingParam("An action is required")
default:
return httputil.InvalidParam("action must be restart, cancel or send")
}
if err != nil {
return delayedError(err, 0)
}
httputil.WriteJSON(w, http.StatusOK, struct{}{})
return nil
}
// DescribeRoomError is the Matrix error a failed delayed send is recorded
// with: the error the client would have got had it sent the event itself.
func DescribeRoomError(err error) json.RawMessage {
var matrixErr *httputil.MatrixError
if !errors.As(roomError(err), &matrixErr) {
matrixErr = httputil.NewError(http.StatusInternalServerError, httputil.ErrCodeUnknown, "%s", err.Error())
}
raw, _ := json.Marshal(matrixErr)
return raw
}
// Compile-time check that the roomserver can be the service's sender.
var _ delayedevents.Sender = (*roomserver.API)(nil)