From c63286713570e1274759db971b15405665fa391a Mon Sep 17 00:00:00 2001 From: Kegsay Date: Tue, 28 Jul 2020 10:09:10 +0100 Subject: [PATCH] Modify /state/{eventType}/{stateKey} to return the event at the time the user left (#1222) * Modify /state/{eventType}/{stateKey} to return the event at the time the user left Or live, depending on their current state. Hopefully fixes some sytests! * Linting * Set HasBeenInRoom * Fix cases for world-readable history visibility * Fix bug in finding the requested state event Co-authored-by: Neil Alexander --- clientapi/routing/routing.go | 4 +- clientapi/routing/state.go | 152 ++++++++++++++++++++++++++++++----- roomserver/internal/query.go | 1 + sytest-whitelist | 1 + 4 files changed, 136 insertions(+), 22 deletions(-) diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index 311f64d1..5724a20c 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -216,7 +216,7 @@ func Setup( eventType = eventType[:len(eventType)-1] } eventFormat := req.URL.Query().Get("format") == "event" - return OnIncomingStateTypeRequest(req.Context(), rsAPI, vars["roomID"], eventType, "", eventFormat) + return OnIncomingStateTypeRequest(req.Context(), device, rsAPI, vars["roomID"], eventType, "", eventFormat) })).Methods(http.MethodGet, http.MethodOptions) r0mux.Handle("/rooms/{roomID}/state/{type}/{stateKey}", httputil.MakeAuthAPI("room_state", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { @@ -225,7 +225,7 @@ func Setup( return util.ErrorResponse(err) } eventFormat := req.URL.Query().Get("format") == "event" - return OnIncomingStateTypeRequest(req.Context(), rsAPI, vars["roomID"], vars["type"], vars["stateKey"], eventFormat) + return OnIncomingStateTypeRequest(req.Context(), device, rsAPI, vars["roomID"], vars["type"], vars["stateKey"], eventFormat) })).Methods(http.MethodGet, http.MethodOptions) r0mux.Handle("/rooms/{roomID}/state/{eventType:[^/]+/?}", diff --git a/clientapi/routing/state.go b/clientapi/routing/state.go index 2ec7a33f..c4f7c4f2 100644 --- a/clientapi/routing/state.go +++ b/clientapi/routing/state.go @@ -17,11 +17,13 @@ package routing import ( "context" "encoding/json" + "fmt" "net/http" "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/syncapi/types" + userapi "github.com/matrix-org/dendrite/userapi/api" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" log "github.com/sirupsen/logrus" @@ -99,40 +101,150 @@ func OnIncomingStateRequest(ctx context.Context, rsAPI api.RoomserverInternalAPI // state to see if there is an event with that type and state key, if there // is then (by default) we return the content, otherwise a 404. // If eventFormat=true, sends the whole event else just the content. -func OnIncomingStateTypeRequest(ctx context.Context, rsAPI api.RoomserverInternalAPI, roomID, evType, stateKey string, eventFormat bool) util.JSONResponse { - // TODO(#287): Auth request and handle the case where the user has left (where - // we should return the state at the poin they left) - util.GetLogger(ctx).WithFields(log.Fields{ - "roomID": roomID, - "evType": evType, - "stateKey": stateKey, - }).Info("Fetching state") +// nolint:gocyclo +func OnIncomingStateTypeRequest( + ctx context.Context, device *userapi.Device, rsAPI api.RoomserverInternalAPI, + roomID, evType, stateKey string, eventFormat bool, +) util.JSONResponse { + var worldReadable bool + var wantLatestState bool - stateReq := api.QueryLatestEventsAndStateRequest{ - RoomID: roomID, - StateToFetch: []gomatrixserverlib.StateKeyTuple{ - gomatrixserverlib.StateKeyTuple{ - EventType: evType, - StateKey: stateKey, - }, + // Always fetch visibility so that we can work out whether to show + // the latest events or the last event from when the user was joined. + // Then include the requested event type and state key, assuming it + // isn't for the same. + stateToFetch := []gomatrixserverlib.StateKeyTuple{ + { + EventType: evType, + StateKey: stateKey, }, } - stateRes := api.QueryLatestEventsAndStateResponse{} + if evType != gomatrixserverlib.MRoomHistoryVisibility && stateKey != "" { + stateToFetch = append(stateToFetch, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomHistoryVisibility, + StateKey: "", + }) + } - if err := rsAPI.QueryLatestEventsAndState(ctx, &stateReq, &stateRes); err != nil { + // First of all, get the latest state of the room. We need to do this + // so that we can look at the history visibility of the room. If the + // room is world-readable then we will always return the latest state. + stateRes := api.QueryLatestEventsAndStateResponse{} + if err := rsAPI.QueryLatestEventsAndState(ctx, &api.QueryLatestEventsAndStateRequest{ + RoomID: roomID, + StateToFetch: stateToFetch, + }, &stateRes); err != nil { util.GetLogger(ctx).WithError(err).Error("queryAPI.QueryLatestEventsAndState failed") return jsonerror.InternalServerError() } - if len(stateRes.StateEvents) == 0 { + // Look at the room state and see if we have a history visibility event + // that marks the room as world-readable. If we don't then we assume that + // the room is not world-readable. + for _, ev := range stateRes.StateEvents { + if ev.Type() == gomatrixserverlib.MRoomHistoryVisibility { + content := map[string]string{} + if err := json.Unmarshal(ev.Content(), &content); err != nil { + util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for history visibility failed") + return jsonerror.InternalServerError() + } + if visibility, ok := content["history_visibility"]; ok { + worldReadable = visibility == "world_readable" + break + } + } + } + + // If the room isn't world-readable then we will instead try to find out + // the state of the room based on the user's membership. If the user is + // in the room then we'll want the latest state. If the user has never + // been in the room and the room isn't world-readable, then we won't + // return any state. If the user was in the room previously but is no + // longer then we will return the state at the time that the user left. + // membershipRes will only be populated if the room is not world-readable. + var membershipRes api.QueryMembershipForUserResponse + if !worldReadable { + // The room isn't world-readable so try to work out based on the + // user's membership if we want the latest state or not. + err := rsAPI.QueryMembershipForUser(ctx, &api.QueryMembershipForUserRequest{ + RoomID: roomID, + UserID: device.UserID, + }, &membershipRes) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser") + return jsonerror.InternalServerError() + } + // If the user has never been in the room then stop at this point. + // We won't tell the user about a room they have never joined. + if !membershipRes.HasBeenInRoom { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden(fmt.Sprintf("Unknown room %q or user %q has never joined this room", roomID, device.UserID)), + } + } + // Otherwise, if the user has been in the room, whether or not we + // give them the latest state will depend on if they are *still* in + // the room. + wantLatestState = membershipRes.IsInRoom + } else { + // The room is world-readable so the user join state is irrelevant, + // just get the latest room state instead. + wantLatestState = true + } + + util.GetLogger(ctx).WithFields(log.Fields{ + "roomID": roomID, + "evType": evType, + "stateKey": stateKey, + "state_at_event": !wantLatestState, + }).Info("Fetching state") + + var event *gomatrixserverlib.HeaderedEvent + if wantLatestState { + // If we are happy to use the latest state, either because the user is + // still in the room, or because the room is world-readable, then just + // use the result of the previous QueryLatestEventsAndState response + // to find the state event, if provided. + for _, ev := range stateRes.StateEvents { + if ev.Type() == evType && ev.StateKeyEquals(stateKey) { + event = &ev + break + } + } + } else { + // Otherwise, take the event ID of their leave event and work out what + // the state of the room was before that event. + var stateAfterRes api.QueryStateAfterEventsResponse + err := rsAPI.QueryStateAfterEvents(ctx, &api.QueryStateAfterEventsRequest{ + RoomID: roomID, + PrevEventIDs: []string{membershipRes.EventID}, + StateToFetch: []gomatrixserverlib.StateKeyTuple{ + { + EventType: evType, + StateKey: stateKey, + }, + }, + }, &stateAfterRes) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser") + return jsonerror.InternalServerError() + } + if len(stateAfterRes.StateEvents) > 0 { + event = &stateAfterRes.StateEvents[0] + } + } + + // If there was no event found that matches all of the above criteria then + // return an error. + if event == nil { return util.JSONResponse{ Code: http.StatusNotFound, - JSON: jsonerror.NotFound("cannot find state"), + JSON: jsonerror.NotFound(fmt.Sprintf("Cannot find state event for %q", evType)), } } stateEvent := stateEventInStateResp{ - ClientEvent: gomatrixserverlib.HeaderedToClientEvent(stateRes.StateEvents[0], gomatrixserverlib.FormatAll), + ClientEvent: gomatrixserverlib.HeaderedToClientEvent(*event, gomatrixserverlib.FormatAll), } var res interface{} diff --git a/roomserver/internal/query.go b/roomserver/internal/query.go index bede6c88..828e5fd3 100644 --- a/roomserver/internal/query.go +++ b/roomserver/internal/query.go @@ -226,6 +226,7 @@ func (r *RoomserverInternalAPI) QueryMembershipForUser( } response.IsInRoom = stillInRoom + response.HasBeenInRoom = true evs, err := r.DB.Events(ctx, []types.EventNID{membershipEventNID}) if err != nil { diff --git a/sytest-whitelist b/sytest-whitelist index 234eae39..5087186b 100644 --- a/sytest-whitelist +++ b/sytest-whitelist @@ -415,3 +415,4 @@ We don't send redundant membership state across incremental syncs by default Typing notifications don't leak Users cannot kick users from a room they are not in Users cannot kick users who have already left a room +Can get 'm.room.name' state for a departed room (SPEC-216)