diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index c259e5293..501eb7eed 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 OnIncomingStateRequest(req.Context(), device, rsAPI, vars["roomID"]) + return OnIncomingStateTypeRequest(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,8 +214,7 @@ func Setup( if strings.HasSuffix(eventType, "/") { eventType = eventType[:len(eventType)-1] } - eventFormat := req.URL.Query().Get("format") == "event" - return OnIncomingStateTypeRequest(req.Context(), device, rsAPI, vars["roomID"], eventType, "", eventFormat) + return OnIncomingStateTypeRequest(req.Context(), device, rsAPI, vars["roomID"], eventType, "") })).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 { @@ -223,8 +222,7 @@ func Setup( if err != nil { return util.ErrorResponse(err) } - eventFormat := req.URL.Query().Get("format") == "event" - return OnIncomingStateTypeRequest(req.Context(), device, rsAPI, vars["roomID"], vars["type"], vars["stateKey"], eventFormat) + return OnIncomingStateTypeRequest(req.Context(), device, rsAPI, vars["roomID"], vars["type"], vars["stateKey"]) })).Methods(http.MethodGet, http.MethodOptions) r0mux.Handle("/rooms/{roomID}/state/{eventType:[^/]+/?}", diff --git a/clientapi/routing/state.go b/clientapi/routing/state.go index 2a424cbe8..152352622 100644 --- a/clientapi/routing/state.go +++ b/clientapi/routing/state.go @@ -28,162 +28,47 @@ import ( log "github.com/sirupsen/logrus" ) -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. +// 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. // nolint:gocyclo func OnIncomingStateTypeRequest( ctx context.Context, device *userapi.Device, rsAPI api.RoomserverInternalAPI, - roomID, evType, stateKey string, eventFormat bool, + roomID, evType, stateKey string, ) 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. - stateToFetch := []gomatrixserverlib.StateKeyTuple{ - { - EventType: evType, - StateKey: stateKey, - }, - } - if evType != gomatrixserverlib.MRoomHistoryVisibility && stateKey != "" { - stateToFetch = append(stateToFetch, gomatrixserverlib.StateKeyTuple{ - EventType: gomatrixserverlib.MRoomHistoryVisibility, - StateKey: "", - }) + stateToFetchIncVisibility := stateToFetch + if !wantAllState && evType != gomatrixserverlib.MRoomHistoryVisibility && stateKey != "" { + stateToFetchIncVisibility = append( + stateToFetchIncVisibility, + gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomHistoryVisibility, + StateKey: "", + }, + ) } // First of all, get the latest state of the room. We need to do this @@ -192,7 +77,7 @@ func OnIncomingStateTypeRequest( stateRes := api.QueryLatestEventsAndStateResponse{} if err := rsAPI.QueryLatestEventsAndState(ctx, &api.QueryLatestEventsAndStateRequest{ RoomID: roomID, - StateToFetch: stateToFetch, + StateToFetch: stateToFetchIncVisibility, }, &stateRes); err != nil { util.GetLogger(ctx).WithError(err).Error("queryAPI.QueryLatestEventsAndState failed") return jsonerror.InternalServerError() @@ -259,7 +144,7 @@ func OnIncomingStateTypeRequest( "state_at_event": !wantLatestState, }).Info("Fetching state") - var event *gomatrixserverlib.HeaderedEvent + var events []*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 @@ -267,7 +152,7 @@ func OnIncomingStateTypeRequest( // to find the state event, if provided. for _, ev := range stateRes.StateEvents { if ev.Type() == evType && ev.StateKeyEquals(stateKey) { - event = &ev + events = append(events, &ev) break } } @@ -278,42 +163,36 @@ func OnIncomingStateTypeRequest( err := rsAPI.QueryStateAfterEvents(ctx, &api.QueryStateAfterEventsRequest{ RoomID: roomID, PrevEventIDs: []string{membershipRes.EventID}, - StateToFetch: []gomatrixserverlib.StateKeyTuple{ - { - EventType: evType, - StateKey: stateKey, - }, - }, + StateToFetch: stateToFetch, }, &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] + events = append(events, &stateAfterRes.StateEvents[0]) } } // If there was no event found that matches all of the above criteria then // return an error. - if event == nil { + if !wantAllState && len(events) == 0 { return util.JSONResponse{ Code: http.StatusNotFound, JSON: jsonerror.NotFound(fmt.Sprintf("Cannot find state event for %q", evType)), } } - stateEvent := stateEventInStateResp{ - ClientEvent: gomatrixserverlib.HeaderedToClientEvent(*event, gomatrixserverlib.FormatAll), - } - - var res interface{} - if eventFormat { - res = stateEvent - } else { - res = stateEvent.Content + // Turn the events into client events. + var res []gomatrixserverlib.ClientEvent + for _, event := range events { + res = append( + res, + gomatrixserverlib.HeaderedToClientEvent(*event, gomatrixserverlib.FormatAll), + ) } + // Return the client events. return util.JSONResponse{ Code: http.StatusOK, JSON: res,