package clientapi import ( "net/http" "strings" "github.com/dariuszjaniak/warp/server/internal/httputil" "github.com/dariuszjaniak/warp/server/internal/matrix/id" "github.com/dariuszjaniak/warp/server/internal/roomserver" "github.com/dariuszjaniak/warp/server/internal/userapi" ) // joinRequest is the body of both join endpoints. type joinRequest struct { Reason string `json:"reason"` // ThirdPartySigned is the proof an identity server issued for a // third-party invite. Warp does not talk to one, so it is refused // rather than ignored. ThirdPartySigned map[string]any `json:"third_party_signed"` } type joinResponse struct { RoomID string `json:"room_id"` } // handleJoinRoomByID implements POST /_matrix/client/v3/rooms/{roomID}/join. func (c *ClientAPI) handleJoinRoomByID(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { roomID, err := pathRoomID(r) if err != nil { return err } return c.join(w, r, auth, roomID) } // handleJoinRoom implements POST /_matrix/client/v3/join/{roomIDOrAlias}, // the endpoint a client uses when all it has is an alias. func (c *ClientAPI) handleJoinRoom(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { target := r.PathValue("roomIDOrAlias") switch { case strings.HasPrefix(target, "!"): if err := id.ValidateRoomID(target); err != nil { return httputil.InvalidParam("%s is not a valid room ID", target) } return c.join(w, r, auth, target) case strings.HasPrefix(target, "#"): resolved, err := c.resolveAlias(r.Context(), target) if err != nil { return roomError(err) } return c.join(w, r, auth, resolved.RoomID) default: return httputil.InvalidParam("%s is neither a room ID nor a room alias", target) } } // join is the body of both join endpoints. func (c *ClientAPI) join(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated, roomID string) error { var req joinRequest if _, err := httputil.DecodeOptionalJSON(w, r, &req); err != nil { return err } if len(req.ThirdPartySigned) > 0 { return httputil.Unknown(http.StatusBadRequest, "Third-party invites are not supported by this server") } joined, err := c.rooms.JoinRoom(r.Context(), roomserver.JoinRequest{ RoomID: roomID, User: auth.UserID, Reason: req.Reason, ServerNames: r.URL.Query()["server_name"], }) if err != nil { return roomError(err) } httputil.WriteJSON(w, http.StatusOK, joinResponse{RoomID: joined}) return nil } // reasonRequest is the body of /leave: a note, at most. type reasonRequest struct { Reason string `json:"reason"` } // handleLeaveRoom implements POST /_matrix/client/v3/rooms/{roomID}/leave. // // The same endpoint rejects an invite, which is why it works on a room the // caller has never been joined to. func (c *ClientAPI) handleLeaveRoom(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { roomID, err := pathRoomID(r) if err != nil { return err } var req reasonRequest if _, err := httputil.DecodeOptionalJSON(w, r, &req); err != nil { return err } if err := c.rooms.LeaveRoom(r.Context(), roomID, auth.UserID, req.Reason); err != nil { return roomError(err) } httputil.WriteJSON(w, http.StatusOK, struct{}{}) return nil } // targetRequest is the body of invite, kick, ban and unban. type targetRequest struct { UserID string `json:"user_id"` Reason string `json:"reason"` // The identity-server fields of /invite. Their presence is what // distinguishes a third-party invite from an ordinary one. IDServer string `json:"id_server"` Medium string `json:"medium"` Address string `json:"address"` IDAccessTok string `json:"id_access_token"` } // handleInvite implements POST /_matrix/client/v3/rooms/{roomID}/invite. func (c *ClientAPI) handleInvite(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { roomID, req, err := c.targetRequest(w, r) if err != nil { return err } if req.Address != "" || req.Medium != "" || req.IDServer != "" { return httputil.Unknown(http.StatusBadRequest, "Third-party invites are not supported by this server") } // A user of this server that does not exist may be one an // application service creates on demand: a bridge's puppet for // somebody on the other network is invited before it is registered. if localpart, err := c.users.ParseLocalUserID(req.UserID); err == nil { if _, err := c.ensureAppServiceUser(r.Context(), localpart); err != nil { return err } } if err := c.rooms.InviteUser(r.Context(), roomserver.InviteRequest{ RoomID: roomID, Sender: auth.UserID, Target: req.UserID, Reason: req.Reason, }); err != nil { return roomError(err) } httputil.WriteJSON(w, http.StatusOK, struct{}{}) return nil } // handleKick implements POST /_matrix/client/v3/rooms/{roomID}/kick. func (c *ClientAPI) handleKick(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { roomID, req, err := c.targetRequest(w, r) if err != nil { return err } if req.UserID == "" { return httputil.MissingParam("A user ID is required") } if err := c.rooms.KickUser(r.Context(), roomID, auth.UserID, req.UserID, req.Reason); err != nil { return roomError(err) } httputil.WriteJSON(w, http.StatusOK, struct{}{}) return nil } // handleBan implements POST /_matrix/client/v3/rooms/{roomID}/ban. func (c *ClientAPI) handleBan(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { roomID, req, err := c.targetRequest(w, r) if err != nil { return err } if req.UserID == "" { return httputil.MissingParam("A user ID is required") } if err := c.rooms.BanUser(r.Context(), roomID, auth.UserID, req.UserID, req.Reason); err != nil { return roomError(err) } httputil.WriteJSON(w, http.StatusOK, struct{}{}) return nil } // handleUnban implements POST /_matrix/client/v3/rooms/{roomID}/unban. func (c *ClientAPI) handleUnban(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { roomID, req, err := c.targetRequest(w, r) if err != nil { return err } if req.UserID == "" { return httputil.MissingParam("A user ID is required") } if err := c.rooms.UnbanUser(r.Context(), roomID, auth.UserID, req.UserID, req.Reason); err != nil { return roomError(err) } httputil.WriteJSON(w, http.StatusOK, struct{}{}) return nil } // targetRequest decodes the body the four moderation endpoints share. func (c *ClientAPI) targetRequest(w http.ResponseWriter, r *http.Request) (string, targetRequest, error) { roomID, err := pathRoomID(r) if err != nil { return "", targetRequest{}, err } var req targetRequest if err := httputil.DecodeJSON(w, r, &req); err != nil { return "", targetRequest{}, err } // A third-party invite has no user_id, which is why the check for one // is left to the handler. return roomID, req, nil }