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)