diff --git a/clientapi/routing/state.go b/clientapi/routing/state.go index 9c2bed12e..5dc05a0a4 100644 --- a/clientapi/routing/state.go +++ b/clientapi/routing/state.go @@ -17,6 +17,7 @@ package routing import ( "context" "encoding/json" + "fmt" "net/http" "github.com/matrix-org/dendrite/clientapi/jsonerror" @@ -100,58 +101,123 @@ 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. +// nolint:gocyclo func OnIncomingStateTypeRequest( ctx context.Context, device *userapi.Device, 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) + var worldReadable bool + var wantLatestState bool - var membershipRes api.QueryMembershipForUserResponse - err := rsAPI.QueryMembershipForUser(ctx, &api.QueryMembershipForUserRequest{ - RoomID: roomID, - UserID: device.UserID, - }, &membershipRes) - if err != nil { - util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser") + // 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. + toFetch := []gomatrixserverlib.StateKeyTuple{ + { + EventType: gomatrixserverlib.MRoomHistoryVisibility, + StateKey: "", + }, + } + if evType != gomatrixserverlib.MRoomHistoryVisibility && stateKey != "" { + toFetch = append(toFetch, gomatrixserverlib.StateKeyTuple{ + EventType: evType, + StateKey: stateKey, + }) + } + + // 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: toFetch, + }, &stateRes); err != nil { + util.GetLogger(ctx).WithError(err).Error("queryAPI.QueryLatestEventsAndState failed") return jsonerror.InternalServerError() } - if !membershipRes.HasBeenInRoom { - return util.JSONResponse{ - Code: http.StatusForbidden, - JSON: jsonerror.Forbidden("Unknown room or user is not in room"), + + // 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 i, 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" + // If the request is for the history visibility of the room + // specifically then keep it in the response, otherwise discard + // it so that the later checks make sense. + if evType != gomatrixserverlib.MRoomHistoryVisibility && stateKey != "" { + stateRes.StateEvents = append(stateRes.StateEvents[:i], stateRes.StateEvents[i+1:]...) + } + 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": !membershipRes.IsInRoom, + "state_at_event": !wantLatestState, }).Info("Fetching state") var event *gomatrixserverlib.HeaderedEvent - if membershipRes.IsInRoom { - stateRes := api.QueryLatestEventsAndStateResponse{} - if err = rsAPI.QueryLatestEventsAndState(ctx, &api.QueryLatestEventsAndStateRequest{ - RoomID: roomID, - StateToFetch: []gomatrixserverlib.StateKeyTuple{ - gomatrixserverlib.StateKeyTuple{ - EventType: evType, - StateKey: stateKey, - }, - }, - }, &stateRes); err != nil { - util.GetLogger(ctx).WithError(err).Error("queryAPI.QueryLatestEventsAndState failed") - return jsonerror.InternalServerError() - } + 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. if len(stateRes.StateEvents) > 0 { event = &stateRes.StateEvents[0] } } else { - // fetch the state at the time they left + // 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{ + err := rsAPI.QueryStateAfterEvents(ctx, &api.QueryStateAfterEventsRequest{ RoomID: roomID, PrevEventIDs: []string{membershipRes.EventID}, StateToFetch: []gomatrixserverlib.StateKeyTuple{ @@ -170,10 +236,12 @@ func OnIncomingStateTypeRequest( } } + // 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)), } } diff --git a/sytest-whitelist b/sytest-whitelist index 234eae39d..5087186b7 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)