diff --git a/clientapi/routing/context.go b/clientapi/routing/context.go new file mode 100644 index 000000000..97de42ce1 --- /dev/null +++ b/clientapi/routing/context.go @@ -0,0 +1,235 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// 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 routing + +import ( + "context" + "database/sql" + "encoding/json" + "net/http" + "strconv" + + "github.com/matrix-org/dendrite/clientapi/jsonerror" + roomserver "github.com/matrix-org/dendrite/roomserver/api" + userapi "github.com/matrix-org/dendrite/userapi/api" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" + "github.com/sirupsen/logrus" +) + +type ContextRespsonse struct { + End string `json:"end,omitempty"` + Event gomatrixserverlib.ClientEvent `json:"event"` + EventsAfter []gomatrixserverlib.ClientEvent `json:"events_after,omitempty"` + EventsBefore []gomatrixserverlib.ClientEvent `json:"events_before,omitempty"` + Start string `json:"start,omitempty"` + State []gomatrixserverlib.ClientEvent `json:"state,omitempty"` +} + +func Context( + req *http.Request, device *userapi.Device, + rsAPI roomserver.RoomserverInternalAPI, + userAPI userapi.UserInternalAPI, + roomID, eventID string, +) util.JSONResponse { + limit, filter, err := parseContextParams(req) + if err != nil { + errMsg := "" + switch err.(type) { + case *json.InvalidUnmarshalError: + errMsg = "unable to parse filter" + case *strconv.NumError: + errMsg = "unable to parse limit" + } + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.InvalidParam(errMsg), + Headers: nil, + } + } + ctx := req.Context() + + membershipRes := roomserver.QueryMembershipForUserResponse{} + membershipReq := roomserver.QueryMembershipForUserRequest{UserID: device.UserID, RoomID: roomID} + if err := rsAPI.QueryMembershipForUser(ctx, &membershipReq, &membershipRes); err != nil { + return jsonerror.InternalServerError() + } + + _ = filter + + avatarTuple := gomatrixserverlib.StateKeyTuple{EventType: "m.room.avatar", StateKey: ""} + nameTuple := gomatrixserverlib.StateKeyTuple{EventType: "m.room.name", StateKey: ""} + canonicalTuple := gomatrixserverlib.StateKeyTuple{EventType: gomatrixserverlib.MRoomCanonicalAlias, StateKey: ""} + topicTuple := gomatrixserverlib.StateKeyTuple{EventType: "m.room.topic", StateKey: ""} + guestTuple := gomatrixserverlib.StateKeyTuple{EventType: "m.room.guest_access", StateKey: ""} + visibilityTuple := gomatrixserverlib.StateKeyTuple{EventType: gomatrixserverlib.MRoomHistoryVisibility, StateKey: ""} + joinRuleTuple := gomatrixserverlib.StateKeyTuple{EventType: gomatrixserverlib.MRoomJoinRules, StateKey: ""} + + currentState := &roomserver.QueryCurrentStateResponse{} + if err := rsAPI.QueryCurrentState(ctx, &roomserver.QueryCurrentStateRequest{ + RoomID: roomID, + StateTuples: []gomatrixserverlib.StateKeyTuple{ + avatarTuple, nameTuple, canonicalTuple, topicTuple, guestTuple, visibilityTuple, joinRuleTuple, + }, + }, currentState); err != nil { + logrus.WithField("roomID", roomID).WithError(err).Error("unable to fetch current state") + return jsonerror.InternalServerError() + } + + state := []gomatrixserverlib.ClientEvent{} + for tuple, event := range currentState.StateEvents { + // check that the user is allowed to view the context + if tuple == visibilityTuple { + hisVis, err := event.HistoryVisibility() + if err != nil { + return jsonerror.InternalServerError() + } + allowed := hisVis != "world_readable" && membershipRes.Membership == "join" + if !allowed { + return util.JSONResponse{ + Code: http.StatusForbidden, + } + } + } + state = append(state, gomatrixserverlib.HeaderedToClientEvent(event, gomatrixserverlib.FormatAll)) + } + + requestEvent := &roomserver.QueryEventsByIDResponse{} + if err := rsAPI.QueryEventsByID(ctx, &roomserver.QueryEventsByIDRequest{ + EventIDs: []string{eventID}, + }, requestEvent); err != nil { + return jsonerror.InternalServerError() + } + if requestEvent.Events == nil || len(requestEvent.Events) == 0 { + logrus.WithField("eventID", eventID).Error("unable to find requested event") + return jsonerror.InternalServerError() + } + // this should be safe now + event := requestEvent.Events[0] + + eventsBefore, err := queryEventsBefore(rsAPI, ctx, event.PrevEventIDs(), limit) + if err != nil && err != sql.ErrNoRows { + logrus.WithError(err).Error("unable to fetch before events") + return jsonerror.InternalServerError() + } + + eventsAfter, err := queryEventsAfter(rsAPI, ctx, event.EventID(), limit) + if err != nil { + logrus.WithError(err).Error("unable to fetch after events") + return jsonerror.InternalServerError() + } + + response := ContextRespsonse{ + End: "end", + Event: gomatrixserverlib.HeaderedToClientEvent(event, gomatrixserverlib.FormatAll), + EventsAfter: eventsAfter, + EventsBefore: eventsBefore, + Start: "start", + State: state, + } + + return util.JSONResponse{ + Code: http.StatusOK, + JSON: response, + } +} + +// queryEventsAfter retrieves events that happened after a list of events. +// The function returns once the limit is reached or no new events can be found. +// TODO: inefficient +func queryEventsAfter( + rsAPI roomserver.RoomserverInternalAPI, + ctx context.Context, + eventID string, + limit int, +) ([]gomatrixserverlib.ClientEvent, error) { + result := []gomatrixserverlib.ClientEvent{} + for { + res := &roomserver.QueryEventsAfterEventIDesponse{} + if err := rsAPI.QueryEventsAfter(ctx, &roomserver.QueryEventsAfterEventIDRequest{EventIDs: eventID}, res); err != nil { + if err == sql.ErrNoRows { + return result, nil + } + return nil, err + } + if len(res.Events) > 0 { + for _, ev := range res.Events { + result = append(result, *ev) + eventID = ev.EventID + } + } + } +} + +// queryEventsBefore retrieves events that happened before a list of events. +// The function returns once the limit is reached or no new prevEvents can be found. +// TODO: inefficient +func queryEventsBefore( + rsAPI roomserver.RoomserverInternalAPI, + ctx context.Context, + prevEventIDs []string, + limit int, +) ([]gomatrixserverlib.ClientEvent, error) { + // query prev events + eventIDs := prevEventIDs + result := []*gomatrixserverlib.HeaderedEvent{} + for len(eventIDs) > 0 { + prevEvents := &roomserver.QueryEventsByIDResponse{} + if err := rsAPI.QueryEventsByID(ctx, &roomserver.QueryEventsByIDRequest{ + EventIDs: eventIDs, + }, prevEvents); err != nil { + return gomatrixserverlib.HeaderedToClientEvents(result, gomatrixserverlib.FormatAll), err + } + // we didn't receive any events, return + if len(prevEvents.Events) == 0 { + return gomatrixserverlib.HeaderedToClientEvents(result, gomatrixserverlib.FormatAll), nil + } + // clear eventIDs to search for + eventIDs = []string{} + // append found events to result + for _, ev := range prevEvents.Events { + result = append(result, ev) + if len(result) >= limit { + return gomatrixserverlib.HeaderedToClientEvents(result, gomatrixserverlib.FormatAll), nil + } + // add prev to new eventIDs + eventIDs = append(eventIDs, ev.PrevEventIDs()...) + } + } + + return gomatrixserverlib.HeaderedToClientEvents(result, gomatrixserverlib.FormatAll), nil +} + +func parseContextParams(req *http.Request) (limit int, filter *gomatrixserverlib.RoomEventFilter, err error) { + l := req.URL.Query().Get("limit") + f := req.URL.Query().Get("filter") + limit = 10 + if l != "" { + limit, err = strconv.Atoi(l) + if err != nil { + return 0, filter, err + } + // not in the spec, but feels like a good idea to have an upper bound limit + if limit > 100 { + limit = 100 + } + } + if f != "" { + if err := json.Unmarshal([]byte(f), &filter); err != nil { + return 0, filter, err + } + } + return limit, filter, nil +} diff --git a/clientapi/routing/context_test.go b/clientapi/routing/context_test.go new file mode 100644 index 000000000..72c67ccbc --- /dev/null +++ b/clientapi/routing/context_test.go @@ -0,0 +1,73 @@ +package routing + +import ( + "net/http" + "reflect" + "testing" + + "github.com/matrix-org/gomatrixserverlib" +) + +func Test_parseContextParams(t *testing.T) { + + noParamsReq, _ := http.NewRequest("GET", "https://localhost:8800/_matrix/client/r0/rooms/!hyi4UaxS9mUXpSG9:localhost:8800/context/%24um_T82QqAXN8PayGiBW7j9WExpqTIQ7-JRq-Q6xpIf8?access_token=5dMB0z4tiulyBvCaIKgyjuWG71ybDiYIwNJVJ2UmxRI", nil) + limit2Req, _ := http.NewRequest("GET", "https://localhost:8800/_matrix/client/r0/rooms/!hyi4UaxS9mUXpSG9:localhost:8800/context/%24um_T82QqAXN8PayGiBW7j9WExpqTIQ7-JRq-Q6xpIf8?access_token=5dMB0z4tiulyBvCaIKgyjuWG71ybDiYIwNJVJ2UmxRI&limit=2", nil) + limit10000Req, _ := http.NewRequest("GET", "https://localhost:8800/_matrix/client/r0/rooms/!hyi4UaxS9mUXpSG9:localhost:8800/context/%24um_T82QqAXN8PayGiBW7j9WExpqTIQ7-JRq-Q6xpIf8?access_token=5dMB0z4tiulyBvCaIKgyjuWG71ybDiYIwNJVJ2UmxRI&limit=10000", nil) + invalidLimitReq, _ := http.NewRequest("GET", "https://localhost:8800/_matrix/client/r0/rooms/!hyi4UaxS9mUXpSG9:localhost:8800/context/%24um_T82QqAXN8PayGiBW7j9WExpqTIQ7-JRq-Q6xpIf8?access_token=5dMB0z4tiulyBvCaIKgyjuWG71ybDiYIwNJVJ2UmxRI&limit=100as", nil) + lazyLoadReq, _ := http.NewRequest("GET", "https://localhost:8800//_matrix/client/r0/rooms/!kvEtX3rFamfwKHO3:localhost:8800/context/%24GjmkRbajRHy8_cxcSbUU4qF_njV8yHeLphI2azTrPaI?limit=2&filter=%7B+%22lazy_load_members%22+%3A+true+%7D&access_token=t1Njzm74w3G40CJ5xrlf1V2haXom0z0Iq1qyyVWhbVo", nil) + invalidFilterReq, _ := http.NewRequest("GET", "https://localhost:8800//_matrix/client/r0/rooms/!kvEtX3rFamfwKHO3:localhost:8800/context/%24GjmkRbajRHy8_cxcSbUU4qF_njV8yHeLphI2azTrPaI?limit=2&filter=%7B+%22lazy_load_members%22+%3A+true&access_token=t1Njzm74w3G40CJ5xrlf1V2haXom0z0Iq1qyyVWhbVo", nil) + tests := []struct { + name string + req *http.Request + wantLimit int + wantFilter *gomatrixserverlib.RoomEventFilter + wantErr bool + }{ + { + name: "no params set", + req: noParamsReq, + wantLimit: 10, + }, + { + name: "limit 2 param set", + req: limit2Req, + wantLimit: 2, + }, + { + name: "limit 10000 param set", + req: limit10000Req, + wantLimit: 100, + }, + { + name: "filter lazy_load_members param set", + req: lazyLoadReq, + wantLimit: 2, + wantFilter: &gomatrixserverlib.RoomEventFilter{LazyLoadMembers: true}, + }, + { + name: "invalid limit req", + req: invalidLimitReq, + wantErr: true, + }, + { + name: "invalid filter req", + req: invalidFilterReq, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotLimit, gotFilter, err := parseContextParams(tt.req) + if (err != nil) != tt.wantErr { + t.Errorf("parseContextParams() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotLimit != tt.wantLimit { + t.Errorf("parseContextParams() gotLimit = %v, want %v", gotLimit, tt.wantLimit) + } + if !reflect.DeepEqual(gotFilter, tt.wantFilter) { + t.Errorf("parseContextParams() gotFilter = %v, want %v", gotFilter, tt.wantFilter) + } + }) + } +} diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index 732066166..4a5240758 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -1111,4 +1111,18 @@ func Setup( return SetReceipt(req, eduAPI, device, vars["roomId"], vars["receiptType"], vars["eventId"]) }), ).Methods(http.MethodPost, http.MethodOptions) + r0mux.Handle("/rooms/{roomId}/context/{eventId}", + httputil.MakeAuthAPI(gomatrixserverlib.Join, userAPI, cfg.Matrix.UserConsentOptions, false, func(req *http.Request, device *userapi.Device) util.JSONResponse { + vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) + if err != nil { + return util.ErrorResponse(err) + } + + return Context( + req, device, + rsAPI, userAPI, + vars["roomId"], vars["eventId"], + ) + }), + ).Methods(http.MethodGet, http.MethodOptions) }