From 8ccc5d108ba418562ce6048f30e69f7791675c5c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 4 Aug 2017 16:32:10 +0100 Subject: [PATCH] Implement membership APIs (#171) * Implement membership endpoints * Use FillBuilder when possible * Fix typo in membership event content * Fix state key invite membership events not being correctly set * Set membership content to match the profile of the user in state_key * Move event building and rename common function * Doc getMembershipStateKey * Check if user is local before lookin up their profile --- .../dendrite/clientapi/events/eventcontent.go | 1 + .../dendrite/clientapi/events/events.go | 87 +++++++++++ .../dendrite/clientapi/readers/profile.go | 38 +---- .../dendrite/clientapi/routing/routing.go | 6 + .../dendrite/clientapi/writers/membership.go | 139 ++++++++++++++++++ .../dendrite/clientapi/writers/sendevent.go | 41 +----- 6 files changed, 241 insertions(+), 71 deletions(-) create mode 100644 src/github.com/matrix-org/dendrite/clientapi/events/events.go create mode 100644 src/github.com/matrix-org/dendrite/clientapi/writers/membership.go diff --git a/src/github.com/matrix-org/dendrite/clientapi/events/eventcontent.go b/src/github.com/matrix-org/dendrite/clientapi/events/eventcontent.go index 8fed23596..e16b54004 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/events/eventcontent.go +++ b/src/github.com/matrix-org/dendrite/clientapi/events/eventcontent.go @@ -25,6 +25,7 @@ type MemberContent struct { Membership string `json:"membership"` DisplayName string `json:"displayname,omitempty"` AvatarURL string `json:"avatar_url,omitempty"` + Reason string `json:"reason,omitempty"` // TODO: ThirdPartyInvite string `json:"third_party_invite,omitempty"` } diff --git a/src/github.com/matrix-org/dendrite/clientapi/events/events.go b/src/github.com/matrix-org/dendrite/clientapi/events/events.go new file mode 100644 index 000000000..49746adcc --- /dev/null +++ b/src/github.com/matrix-org/dendrite/clientapi/events/events.go @@ -0,0 +1,87 @@ +// Copyright 2017 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package events + +import ( + "errors" + "fmt" + "time" + + "github.com/matrix-org/dendrite/common/config" + "github.com/matrix-org/dendrite/roomserver/api" + + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" +) + +// ErrRoomNoExists is returned when trying to lookup the state of a room that +// doesn't exist +var ErrRoomNoExists = errors.New("Room does not exist") + +// BuildEvent builds a Matrix event using the event builder and roomserver query +// API client provided. If also fills roomserver query API response (if provided) +// in case the function calling FillBuilder needs to use it. +// Returns ErrRoomNoExists if the state of the room could not be retrieved because +// the room doesn't exist +// Returns an error if something else went wrong +func BuildEvent( + builder *gomatrixserverlib.EventBuilder, cfg config.Dendrite, + queryAPI api.RoomserverQueryAPI, queryRes *api.QueryLatestEventsAndStateResponse, +) (*gomatrixserverlib.Event, error) { + eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder) + if err != nil { + return nil, err + } + + // Ask the roomserver for information about this room + queryReq := api.QueryLatestEventsAndStateRequest{ + RoomID: builder.RoomID, + StateToFetch: eventsNeeded.Tuples(), + } + if queryRes == nil { + queryRes = &api.QueryLatestEventsAndStateResponse{} + } + if queryErr := queryAPI.QueryLatestEventsAndState(&queryReq, queryRes); queryErr != nil { + return nil, err + } + + if !queryRes.RoomExists { + return nil, ErrRoomNoExists + } + + builder.Depth = queryRes.Depth + builder.PrevEvents = queryRes.LatestEvents + + authEvents := gomatrixserverlib.NewAuthEvents(nil) + + for i := range queryRes.StateEvents { + authEvents.AddEvent(&queryRes.StateEvents[i]) + } + + refs, err := eventsNeeded.AuthEventReferences(&authEvents) + if err != nil { + return nil, err + } + builder.AuthEvents = refs + + eventID := fmt.Sprintf("$%s:%s", util.RandomString(16), cfg.Matrix.ServerName) + now := time.Now() + event, err := builder.Build(eventID, now, cfg.Matrix.ServerName, cfg.Matrix.KeyID, cfg.Matrix.PrivateKey) + if err != nil { + return nil, err + } + + return &event, nil +} diff --git a/src/github.com/matrix-org/dendrite/clientapi/readers/profile.go b/src/github.com/matrix-org/dendrite/clientapi/readers/profile.go index a449c38d8..15ca4961b 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/readers/profile.go +++ b/src/github.com/matrix-org/dendrite/clientapi/readers/profile.go @@ -15,9 +15,7 @@ package readers import ( - "fmt" "net/http" - "time" "github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" @@ -284,44 +282,12 @@ func buildMembershipEvents( return nil, err } - eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(&builder) + event, err := events.BuildEvent(&builder, *cfg, queryAPI, nil) if err != nil { return nil, err } - // Ask the roomserver for information about this room - queryReq := api.QueryLatestEventsAndStateRequest{ - RoomID: membership.RoomID, - StateToFetch: eventsNeeded.Tuples(), - } - var queryRes api.QueryLatestEventsAndStateResponse - if queryErr := queryAPI.QueryLatestEventsAndState(&queryReq, &queryRes); queryErr != nil { - return nil, err - } - - builder.Depth = queryRes.Depth - builder.PrevEvents = queryRes.LatestEvents - - authEvents := gomatrixserverlib.NewAuthEvents(nil) - - for i := range queryRes.StateEvents { - authEvents.AddEvent(&queryRes.StateEvents[i]) - } - - refs, err := eventsNeeded.AuthEventReferences(&authEvents) - if err != nil { - return nil, err - } - builder.AuthEvents = refs - - eventID := fmt.Sprintf("$%s:%s", util.RandomString(16), cfg.Matrix.ServerName) - now := time.Now() - event, err := builder.Build(eventID, now, cfg.Matrix.ServerName, cfg.Matrix.KeyID, cfg.Matrix.PrivateKey) - if err != nil { - return nil, err - } - - evs = append(evs, event) + evs = append(evs, *event) } return evs, nil diff --git a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go index 693aba1ad..68a9de075 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go @@ -81,6 +81,12 @@ func Setup( ) }), ) + r0mux.Handle("/rooms/{roomID}/{membership:(?:join|kick|ban|unban|leave|invite)}", + common.MakeAuthAPI("membership", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse { + vars := mux.Vars(req) + return writers.SendMembership(req, accountDB, device, vars["roomID"], vars["membership"], cfg, queryAPI, producer) + }), + ).Methods("POST", "OPTIONS") r0mux.Handle("/rooms/{roomID}/send/{eventType}/{txnID}", common.MakeAuthAPI("send_message", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse { vars := mux.Vars(req) diff --git a/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go b/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go new file mode 100644 index 000000000..7b199a606 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go @@ -0,0 +1,139 @@ +// Copyright 2017 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package writers + +import ( + "net/http" + + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" + "github.com/matrix-org/dendrite/clientapi/events" + "github.com/matrix-org/dendrite/clientapi/httputil" + "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/clientapi/producers" + "github.com/matrix-org/dendrite/common/config" + "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/gomatrixserverlib" + + "github.com/matrix-org/util" +) + +// SendMembership implements PUT /rooms/{roomID}/(join|kick|ban|unban|leave|invite) +// by building a m.room.member event then sending it to the room server +func SendMembership( + req *http.Request, accountDB *accounts.Database, device *authtypes.Device, + roomID string, membership string, cfg config.Dendrite, + queryAPI api.RoomserverQueryAPI, producer *producers.RoomserverProducer, +) util.JSONResponse { + stateKey, reason, reqErr := getMembershipStateKey(req, device, membership) + if reqErr != nil { + return *reqErr + } + + localpart, serverName, err := gomatrixserverlib.SplitID('@', stateKey) + if err != nil { + return httputil.LogThenError(req, err) + } + + var profile *authtypes.Profile + if serverName == cfg.Matrix.ServerName { + profile, err = accountDB.GetProfileByLocalpart(localpart) + if err != nil { + return httputil.LogThenError(req, err) + } + } else { + profile = &authtypes.Profile{} + } + + builder := gomatrixserverlib.EventBuilder{ + Sender: device.UserID, + RoomID: roomID, + Type: "m.room.member", + StateKey: &stateKey, + } + + // "unban" or "kick" isn't a valid membership value, change it to "leave" + if membership == "unban" || membership == "kick" { + membership = "leave" + } + + content := events.MemberContent{ + Membership: membership, + DisplayName: profile.DisplayName, + AvatarURL: profile.AvatarURL, + Reason: reason, + } + + if err = builder.SetContent(content); err != nil { + return httputil.LogThenError(req, err) + } + + event, err := events.BuildEvent(&builder, cfg, queryAPI, nil) + if err == events.ErrRoomNoExists { + return util.JSONResponse{ + Code: 404, + JSON: jsonerror.NotFound(err.Error()), + } + } else if err != nil { + return httputil.LogThenError(req, err) + } + + if err := producer.SendEvents([]gomatrixserverlib.Event{*event}, cfg.Matrix.ServerName); err != nil { + return httputil.LogThenError(req, err) + } + + return util.JSONResponse{ + Code: 200, + JSON: struct{}{}, + } +} + +// getMembershipStateKey extracts the target user ID of a membership change. +// For "join" and "leave" this will be the ID of the user making the change. +// For "ban", "unban", "kick" and "invite" the target user ID will be in the JSON request body. +// In the latter case, if there was an issue retrieving the user ID from the request body, +// returns a JSONResponse with a corresponding error code and message. +func getMembershipStateKey( + req *http.Request, device *authtypes.Device, membership string, +) (stateKey string, reason string, response *util.JSONResponse) { + if membership == "ban" || membership == "unban" || membership == "kick" || membership == "invite" { + // If we're in this case, the state key is contained in the request body, + // possibly along with a reason (for "kick" and "ban") so we need to parse + // it + var requestBody struct { + UserID string `json:"user_id"` + Reason string `json:"reason"` + } + + if reqErr := httputil.UnmarshalJSONRequest(req, &requestBody); reqErr != nil { + response = reqErr + return + } + if requestBody.UserID == "" { + response = &util.JSONResponse{ + Code: 400, + JSON: jsonerror.BadJSON("'user_id' must be supplied."), + } + return + } + + stateKey = requestBody.UserID + reason = requestBody.Reason + } else { + stateKey = device.UserID + } + + return +} diff --git a/src/github.com/matrix-org/dendrite/clientapi/writers/sendevent.go b/src/github.com/matrix-org/dendrite/clientapi/writers/sendevent.go index 3a4b50c7c..868700f55 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/writers/sendevent.go +++ b/src/github.com/matrix-org/dendrite/clientapi/writers/sendevent.go @@ -17,10 +17,8 @@ package writers import ( "net/http" - "fmt" - "time" - "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/clientapi/events" "github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/producers" @@ -64,41 +62,14 @@ func SendEvent( } builder.SetContent(r) - // work out what will be required in order to send this event - needed, err := gomatrixserverlib.StateNeededForEventBuilder(&builder) - if err != nil { - return httputil.LogThenError(req, err) - } - - // Ask the roomserver for information about this room - queryReq := api.QueryLatestEventsAndStateRequest{ - RoomID: roomID, - StateToFetch: needed.Tuples(), - } var queryRes api.QueryLatestEventsAndStateResponse - if queryErr := queryAPI.QueryLatestEventsAndState(&queryReq, &queryRes); queryErr != nil { - return httputil.LogThenError(req, queryErr) - } - if !queryRes.RoomExists { + e, err := events.BuildEvent(&builder, cfg, queryAPI, &queryRes) + if err == events.ErrRoomNoExists { return util.JSONResponse{ Code: 404, JSON: jsonerror.NotFound("Room does not exist"), } - } - - // set the fields we previously couldn't do and build the event - builder.PrevEvents = queryRes.LatestEvents // the current events will be the prev events of the new event - var refs []gomatrixserverlib.EventReference - for _, e := range queryRes.StateEvents { - refs = append(refs, e.EventReference()) - } - builder.AuthEvents = refs - builder.Depth = queryRes.Depth - eventID := fmt.Sprintf("$%s:%s", util.RandomString(16), cfg.Matrix.ServerName) - e, err := builder.Build( - eventID, time.Now(), cfg.Matrix.ServerName, cfg.Matrix.KeyID, cfg.Matrix.PrivateKey, - ) - if err != nil { + } else if err != nil { return httputil.LogThenError(req, err) } @@ -108,7 +79,7 @@ func SendEvent( stateEvents[i] = &queryRes.StateEvents[i] } provider := gomatrixserverlib.NewAuthEvents(stateEvents) - if err = gomatrixserverlib.Allowed(e, &provider); err != nil { + if err = gomatrixserverlib.Allowed(*e, &provider); err != nil { return util.JSONResponse{ Code: 403, JSON: jsonerror.Forbidden(err.Error()), // TODO: Is this error string comprehensible to the client? @@ -116,7 +87,7 @@ func SendEvent( } // pass the new event to the roomserver - if err := producer.SendEvents([]gomatrixserverlib.Event{e}, cfg.Matrix.ServerName); err != nil { + if err := producer.SendEvents([]gomatrixserverlib.Event{*e}, cfg.Matrix.ServerName); err != nil { return httputil.LogThenError(req, err) }