package clientapi import ( "encoding/json" "net/http" "time" "github.com/dariuszjaniak/warp/server/internal/httputil" "github.com/dariuszjaniak/warp/server/internal/syncapi" "github.com/dariuszjaniak/warp/server/internal/userapi" ) // The endpoints behind the streams that are not room events: typing, // receipts, read markers, to-device messages and presence. // // They are all writes, and all of them are writes a client makes constantly // — a typing notification per keystroke-burst, a receipt per message read. // None of them creates an event, which is why none of them goes through the // roomserver. // typingRequest is the PUT /rooms/{roomID}/typing/{userID} body. type typingRequest struct { Typing bool `json:"typing"` // Timeout is in milliseconds, and is only meaningful when Typing is // true. Timeout int64 `json:"timeout"` } // handlePutTyping implements // PUT /_matrix/client/v3/rooms/{roomID}/typing/{userID}. func (c *ClientAPI) handlePutTyping(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { roomID, err := pathRoomID(r) if err != nil { return err } if err := requireOwnUserID(auth, r.PathValue("userID")); err != nil { return err } var req typingRequest if err := httputil.DecodeJSON(w, r, &req); err != nil { return err } if req.Timeout < 0 { return httputil.InvalidParam("timeout must not be negative") } if err := c.sync.SetTyping(r.Context(), syncapi.TypingRequest{ RoomID: roomID, User: auth.UserID, Typing: req.Typing, Timeout: time.Duration(req.Timeout) * time.Millisecond, }); err != nil { return syncError(err) } httputil.WriteJSON(w, http.StatusOK, struct{}{}) return nil } // receiptRequest is the POST /rooms/{roomID}/receipt/{receiptType}/{eventID} // body. Every field is optional; the only one defined is the thread. type receiptRequest struct { ThreadID string `json:"thread_id"` } // handlePostReceipt implements // POST /_matrix/client/v3/rooms/{roomID}/receipt/{receiptType}/{eventID}. func (c *ClientAPI) handlePostReceipt(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { roomID, err := pathRoomID(r) if err != nil { return err } receiptType := r.PathValue("receiptType") if receiptType == "" { return httputil.MissingParam("A receipt type is required") } eventID := r.PathValue("eventID") if eventID == "" { return httputil.MissingParam("An event ID is required") } // The body is optional: a client marking a room read sends none. var req receiptRequest if _, err := httputil.DecodeOptionalJSON(w, r, &req); err != nil { return err } if err := c.sync.SetReceipt(r.Context(), syncapi.ReceiptRequest{ RoomID: roomID, User: auth.UserID, ReceiptType: receiptType, EventID: eventID, ThreadID: req.ThreadID, }); err != nil { return syncError(err) } httputil.WriteJSON(w, http.StatusOK, struct{}{}) return nil } // readMarkersRequest is the POST /rooms/{roomID}/read_markers body. type readMarkersRequest struct { FullyRead string `json:"m.fully_read"` Read string `json:"m.read"` ReadPrivate string `json:"m.read.private"` } // handlePostReadMarkers implements // POST /_matrix/client/v3/rooms/{roomID}/read_markers. func (c *ClientAPI) handlePostReadMarkers(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { roomID, err := pathRoomID(r) if err != nil { return err } var req readMarkersRequest if err := httputil.DecodeJSON(w, r, &req); err != nil { return err } if req.FullyRead == "" && req.Read == "" && req.ReadPrivate == "" { return httputil.MissingParam( "At least one of m.fully_read, m.read or m.read.private is required") } if err := c.sync.SetReadMarkers(r.Context(), syncapi.ReadMarkersRequest{ RoomID: roomID, User: auth.UserID, FullyRead: req.FullyRead, Read: req.Read, ReadPrivate: req.ReadPrivate, }); err != nil { return syncError(err) } httputil.WriteJSON(w, http.StatusOK, struct{}{}) return nil } // sendToDeviceRequest is the PUT /sendToDevice/{eventType}/{txnID} body. type sendToDeviceRequest struct { Messages map[string]map[string]json.RawMessage `json:"messages"` } // handleSendToDevice implements // PUT /_matrix/client/v3/sendToDevice/{eventType}/{txnID}. // // The transaction ID is accepted and not used. Its purpose is idempotency, // and a to-device message is already idempotent in the way that matters: a // duplicate is a second copy of a key share, which the receiving client // discards. Recording the transaction would mean a table whose only reader // would be a retry that costs nothing anyway. func (c *ClientAPI) handleSendToDevice(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { eventType := r.PathValue("eventType") if eventType == "" { return httputil.MissingParam("An event type is required") } if r.PathValue("txnID") == "" { return httputil.MissingParam("A transaction ID is required") } var req sendToDeviceRequest if err := httputil.DecodeJSON(w, r, &req); err != nil { return err } if err := c.sync.SendToDevice(r.Context(), syncapi.SendToDeviceRequest{ Sender: auth.UserID, EventType: eventType, Messages: req.Messages, }); err != nil { return syncError(err) } httputil.WriteJSON(w, http.StatusOK, struct{}{}) return nil } // presenceRequest is the PUT /presence/{userID}/status body. type presenceRequest struct { Presence string `json:"presence"` // StatusMsg is a pointer so that a request which does not mention it // can be told from one that clears it. StatusMsg *string `json:"status_msg"` } // presenceResponse is what GET /presence/{userID}/status returns. type presenceResponse struct { Presence string `json:"presence"` StatusMsg string `json:"status_msg,omitempty"` // LastActiveAgo is milliseconds since the user was last seen, left out // for a user nothing is known about. LastActiveAgo *int64 `json:"last_active_ago,omitempty"` // CurrentlyActive is only meaningful while the user is online. CurrentlyActive bool `json:"currently_active,omitempty"` } // handlePutPresence implements // PUT /_matrix/client/v3/presence/{userID}/status. func (c *ClientAPI) handlePutPresence(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { if err := requireOwnUserID(auth, r.PathValue("userID")); err != nil { return err } var req presenceRequest if err := httputil.DecodeJSON(w, r, &req); err != nil { return err } if req.Presence == "" { return httputil.MissingParam("A presence state is required") } if err := c.sync.SetPresence(r.Context(), syncapi.PresenceRequest{ User: auth.UserID, Presence: req.Presence, StatusMsg: req.StatusMsg, }); err != nil { return syncError(err) } httputil.WriteJSON(w, http.StatusOK, struct{}{}) return nil } // handleGetPresence implements // GET /_matrix/client/v3/presence/{userID}/status. // // Anybody may ask about anybody: the spec has no visibility rule on // presence beyond the server choosing who to publish changes to, and a // client showing a room's member list asks about every member. func (c *ClientAPI) handleGetPresence(w http.ResponseWriter, r *http.Request, _ userapi.Authenticated) error { userID := r.PathValue("userID") if userID == "" { return httputil.MissingParam("A user ID is required") } state, err := c.sync.Presence(r.Context(), userID) if err != nil { return syncError(err) } resp := presenceResponse{ Presence: state.Presence, StatusMsg: state.StatusMsg, CurrentlyActive: state.Presence == syncapi.PresenceOnline, } if state.LastActiveMS > 0 { ago := max(c.now().UnixMilli()-state.LastActiveMS, 0) resp.LastActiveAgo = &ago } httputil.WriteJSON(w, http.StatusOK, resp) return nil } // requireOwnUserID insists that the user ID in a path is the caller's. // // Typing and presence are claims about the caller, not data belonging to // them, so unlike a profile an admin may not set them for somebody else: // there is no administrative reason to say that another user is typing. func requireOwnUserID(auth userapi.Authenticated, pathUserID string) error { if pathUserID == "" { return httputil.MissingParam("A user ID is required") } if pathUserID != auth.UserID { return httputil.Forbidden("Cannot act on behalf of another user") } return nil }