From 078c27aef198cd30f64575c682914887b7b386d0 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Tue, 25 Aug 2020 18:40:17 +0100 Subject: [PATCH] Revert "Deduplicate OnIncomingStateRequest and OnIncomingStateTypeRequest" This reverts commit 335035d66e629022232abc682d6631e3cf669e23. --- clientapi/routing/routing.go | 8 +- clientapi/routing/state.go | 209 +++++++++++++++++++++++++++-------- 2 files changed, 170 insertions(+), 47 deletions(-) diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index 501eb7eed..c259e5293 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -201,7 +201,7 @@ func Setup( if err != nil { return util.ErrorResponse(err) } - return OnIncomingStateTypeRequest(req.Context(), device, rsAPI, vars["roomID"], "", "") + return OnIncomingStateRequest(req.Context(), device, rsAPI, vars["roomID"]) })).Methods(http.MethodGet, http.MethodOptions) r0mux.Handle("/rooms/{roomID}/state/{type:[^/]+/?}", httputil.MakeAuthAPI("room_state", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { @@ -214,7 +214,8 @@ func Setup( if strings.HasSuffix(eventType, "/") { eventType = eventType[:len(eventType)-1] } - return OnIncomingStateTypeRequest(req.Context(), device, rsAPI, vars["roomID"], eventType, "") + eventFormat := req.URL.Query().Get("format") == "event" + 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 { @@ -222,7 +223,8 @@ func Setup( if err != nil { return util.ErrorResponse(err) } - return OnIncomingStateTypeRequest(req.Context(), device, rsAPI, vars["roomID"], vars["type"], vars["stateKey"]) + eventFormat := req.URL.Query().Get("format") == "event" + 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 152352622..2a424cbe8 100644 --- a/clientapi/routing/state.go +++ b/clientapi/routing/state.go @@ -28,47 +28,162 @@ import ( log "github.com/sirupsen/logrus" ) -// OnIncomingStateTypeRequest is called when a client makes a request to either: -// - /rooms/{roomID}/state -// - /rooms/{roomID}/state/{type}/{statekey} -// The former will have evType and stateKey as "". The latter will specify them. -// It will look in current state to see if there is an event with that type -// and state key, if there is then (by default) we return the content. If the -// history visibility restricts the room state then users who have left or -// have been kicked/banned from the room will see the state from the time that -// they left. +type stateEventInStateResp struct { + gomatrixserverlib.ClientEvent + PrevContent json.RawMessage `json:"prev_content,omitempty"` + ReplacesState string `json:"replaces_state,omitempty"` +} + +// OnIncomingStateRequest is called when a client makes a /rooms/{roomID}/state +// request. It will fetch all the state events from the specified room and will +// append the necessary keys to them if applicable before returning them. +// Returns an error if something went wrong in the process. +// TODO: Check if the user is in the room. If not, check if the room's history +// is publicly visible. Current behaviour is returning an empty array if the +// user cannot see the room's history. +func OnIncomingStateRequest(ctx context.Context, device *userapi.Device, rsAPI api.RoomserverInternalAPI, roomID string) util.JSONResponse { + var worldReadable bool + var wantLatestState bool + + // 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: []gomatrixserverlib.StateKeyTuple{}, + }, &stateRes); err != nil { + util.GetLogger(ctx).WithError(err).Error("queryAPI.QueryLatestEventsAndState failed") + return jsonerror.InternalServerError() + } + + // 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, + "state_at_event": !wantLatestState, + }).Info("Fetching all state") + + stateEvents := []gomatrixserverlib.ClientEvent{} + 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 { + stateEvents = append( + stateEvents, + gomatrixserverlib.HeaderedToClientEvent(ev, gomatrixserverlib.FormatAll), + ) + } + } 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{}, + }, &stateAfterRes) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser") + return jsonerror.InternalServerError() + } + for _, ev := range stateRes.StateEvents { + stateEvents = append( + stateEvents, + gomatrixserverlib.HeaderedToClientEvent(ev, gomatrixserverlib.FormatAll), + ) + } + } + + // Return the results to the requestor. + return util.JSONResponse{ + Code: http.StatusOK, + JSON: stateEvents, + } +} + +// OnIncomingStateTypeRequest is called when a client makes a +// /rooms/{roomID}/state/{type}/{statekey} request. It will look in current +// 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. // nolint:gocyclo func OnIncomingStateTypeRequest( ctx context.Context, device *userapi.Device, rsAPI api.RoomserverInternalAPI, - roomID, evType, stateKey string, + roomID, evType, stateKey string, eventFormat bool, ) util.JSONResponse { var worldReadable bool var wantLatestState bool - wantAllState := evType == "" - - // Work out the state to fetch. If we want the entire room state then - // we leave the stateToFetch empty. Otherwise we specify what we want. - stateToFetch := []gomatrixserverlib.StateKeyTuple{} - if !wantAllState { - stateToFetch = append(stateToFetch, 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. - stateToFetchIncVisibility := stateToFetch - if !wantAllState && evType != gomatrixserverlib.MRoomHistoryVisibility && stateKey != "" { - stateToFetchIncVisibility = append( - stateToFetchIncVisibility, - gomatrixserverlib.StateKeyTuple{ - EventType: gomatrixserverlib.MRoomHistoryVisibility, - StateKey: "", - }, - ) + stateToFetch := []gomatrixserverlib.StateKeyTuple{ + { + EventType: evType, + StateKey: stateKey, + }, + } + if evType != gomatrixserverlib.MRoomHistoryVisibility && stateKey != "" { + stateToFetch = append(stateToFetch, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomHistoryVisibility, + StateKey: "", + }) } // First of all, get the latest state of the room. We need to do this @@ -77,7 +192,7 @@ func OnIncomingStateTypeRequest( stateRes := api.QueryLatestEventsAndStateResponse{} if err := rsAPI.QueryLatestEventsAndState(ctx, &api.QueryLatestEventsAndStateRequest{ RoomID: roomID, - StateToFetch: stateToFetchIncVisibility, + StateToFetch: stateToFetch, }, &stateRes); err != nil { util.GetLogger(ctx).WithError(err).Error("queryAPI.QueryLatestEventsAndState failed") return jsonerror.InternalServerError() @@ -144,7 +259,7 @@ func OnIncomingStateTypeRequest( "state_at_event": !wantLatestState, }).Info("Fetching state") - var events []*gomatrixserverlib.HeaderedEvent + 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 @@ -152,7 +267,7 @@ func OnIncomingStateTypeRequest( // to find the state event, if provided. for _, ev := range stateRes.StateEvents { if ev.Type() == evType && ev.StateKeyEquals(stateKey) { - events = append(events, &ev) + event = &ev break } } @@ -163,36 +278,42 @@ func OnIncomingStateTypeRequest( err := rsAPI.QueryStateAfterEvents(ctx, &api.QueryStateAfterEventsRequest{ RoomID: roomID, PrevEventIDs: []string{membershipRes.EventID}, - StateToFetch: stateToFetch, + 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 { - events = append(events, &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 !wantAllState && len(events) == 0 { + if event == nil { return util.JSONResponse{ Code: http.StatusNotFound, JSON: jsonerror.NotFound(fmt.Sprintf("Cannot find state event for %q", evType)), } } - // Turn the events into client events. - var res []gomatrixserverlib.ClientEvent - for _, event := range events { - res = append( - res, - gomatrixserverlib.HeaderedToClientEvent(*event, gomatrixserverlib.FormatAll), - ) + stateEvent := stateEventInStateResp{ + ClientEvent: gomatrixserverlib.HeaderedToClientEvent(*event, gomatrixserverlib.FormatAll), + } + + var res interface{} + if eventFormat { + res = stateEvent + } else { + res = stateEvent.Content } - // Return the client events. return util.JSONResponse{ Code: http.StatusOK, JSON: res,