diff options
Diffstat (limited to 'roomserver/internal/perform/perform_invite.go')
-rw-r--r-- | roomserver/internal/perform/perform_invite.go | 243 |
1 files changed, 243 insertions, 0 deletions
diff --git a/roomserver/internal/perform/perform_invite.go b/roomserver/internal/perform/perform_invite.go new file mode 100644 index 00000000..7320388e --- /dev/null +++ b/roomserver/internal/perform/perform_invite.go @@ -0,0 +1,243 @@ +package perform + +import ( + "context" + "fmt" + + federationSenderAPI "github.com/matrix-org/dendrite/federationsender/api" + "github.com/matrix-org/dendrite/internal/config" + "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/dendrite/roomserver/internal/helpers" + "github.com/matrix-org/dendrite/roomserver/state" + "github.com/matrix-org/dendrite/roomserver/storage" + "github.com/matrix-org/dendrite/roomserver/types" + "github.com/matrix-org/gomatrixserverlib" + log "github.com/sirupsen/logrus" +) + +type Inviter struct { + DB storage.Database + Cfg *config.RoomServer + FSAPI federationSenderAPI.FederationSenderInternalAPI + + // TODO FIXME: Remove this + RSAPI api.RoomserverInternalAPI +} + +// nolint:gocyclo +func (r *Inviter) PerformInvite( + ctx context.Context, + req *api.PerformInviteRequest, + res *api.PerformInviteResponse, +) ([]api.OutputEvent, error) { + event := req.Event + if event.StateKey() == nil { + return nil, fmt.Errorf("invite must be a state event") + } + + roomID := event.RoomID() + targetUserID := *event.StateKey() + info, err := r.DB.RoomInfo(ctx, roomID) + if err != nil { + return nil, fmt.Errorf("Failed to load RoomInfo: %w", err) + } + + log.WithFields(log.Fields{ + "event_id": event.EventID(), + "room_id": roomID, + "room_version": req.RoomVersion, + "target_user_id": targetUserID, + "room_info_exists": info != nil, + }).Info("processing invite event") + + _, domain, _ := gomatrixserverlib.SplitID('@', targetUserID) + isTargetLocal := domain == r.Cfg.Matrix.ServerName + isOriginLocal := event.Origin() == r.Cfg.Matrix.ServerName + + inviteState := req.InviteRoomState + if len(inviteState) == 0 && info != nil { + var is []gomatrixserverlib.InviteV2StrippedState + if is, err = buildInviteStrippedState(ctx, r.DB, info, req); err == nil { + inviteState = is + } + } + if len(inviteState) == 0 { + if err = event.SetUnsignedField("invite_room_state", struct{}{}); err != nil { + return nil, fmt.Errorf("event.SetUnsignedField: %w", err) + } + } else { + if err = event.SetUnsignedField("invite_room_state", inviteState); err != nil { + return nil, fmt.Errorf("event.SetUnsignedField: %w", err) + } + } + + var isAlreadyJoined bool + if info != nil { + _, isAlreadyJoined, err = r.DB.GetMembership(ctx, info.RoomNID, *event.StateKey()) + if err != nil { + return nil, fmt.Errorf("r.DB.GetMembership: %w", err) + } + } + if isAlreadyJoined { + // If the user is joined to the room then that takes precedence over this + // invite event. It makes little sense to move a user that is already + // joined to the room into the invite state. + // This could plausibly happen if an invite request raced with a join + // request for a user. For example if a user was invited to a public + // room and they joined the room at the same time as the invite was sent. + // The other way this could plausibly happen is if an invite raced with + // a kick. For example if a user was kicked from a room in error and in + // response someone else in the room re-invited them then it is possible + // for the invite request to race with the leave event so that the + // target receives invite before it learns that it has been kicked. + // There are a few ways this could be plausibly handled in the roomserver. + // 1) Store the invite, but mark it as retired. That will result in the + // permanent rejection of that invite event. So even if the target + // user leaves the room and the invite is retransmitted it will be + // ignored. However a new invite with a new event ID would still be + // accepted. + // 2) Silently discard the invite event. This means that if the event + // was retransmitted at a later date after the target user had left + // the room we would accept the invite. However since we hadn't told + // the sending server that the invite had been discarded it would + // have no reason to attempt to retry. + // 3) Signal the sending server that the user is already joined to the + // room. + // For now we will implement option 2. Since in the abesence of a retry + // mechanism it will be equivalent to option 1, and we don't have a + // signalling mechanism to implement option 3. + res.Error = &api.PerformError{ + Code: api.PerformErrorNotAllowed, + Msg: "User is already joined to room", + } + return nil, nil + } + + if isOriginLocal { + // The invite originated locally. Therefore we have a responsibility to + // try and see if the user is allowed to make this invite. We can't do + // this for invites coming in over federation - we have to take those on + // trust. + _, err = helpers.CheckAuthEvents(ctx, r.DB, event, event.AuthEventIDs()) + if err != nil { + log.WithError(err).WithField("event_id", event.EventID()).WithField("auth_event_ids", event.AuthEventIDs()).Error( + "processInviteEvent.checkAuthEvents failed for event", + ) + if _, ok := err.(*gomatrixserverlib.NotAllowed); ok { + res.Error = &api.PerformError{ + Msg: err.Error(), + Code: api.PerformErrorNotAllowed, + } + return nil, nil + } + return nil, fmt.Errorf("checkAuthEvents: %w", err) + } + + // If the invite originated from us and the target isn't local then we + // should try and send the invite over federation first. It might be + // that the remote user doesn't exist, in which case we can give up + // processing here. + if req.SendAsServer != api.DoNotSendToOtherServers && !isTargetLocal { + fsReq := &federationSenderAPI.PerformInviteRequest{ + RoomVersion: req.RoomVersion, + Event: event, + InviteRoomState: inviteState, + } + fsRes := &federationSenderAPI.PerformInviteResponse{} + if err = r.FSAPI.PerformInvite(ctx, fsReq, fsRes); err != nil { + res.Error = &api.PerformError{ + Msg: err.Error(), + Code: api.PerformErrorNoOperation, + } + log.WithError(err).WithField("event_id", event.EventID()).Error("r.FSAPI.PerformInvite failed") + return nil, nil + } + event = fsRes.Event + } + + // Send the invite event to the roomserver input stream. This will + // notify existing users in the room about the invite, update the + // membership table and ensure that the event is ready and available + // to use as an auth event when accepting the invite. + inputReq := &api.InputRoomEventsRequest{ + InputRoomEvents: []api.InputRoomEvent{ + { + Kind: api.KindNew, + Event: event, + AuthEventIDs: event.AuthEventIDs(), + SendAsServer: req.SendAsServer, + }, + }, + } + inputRes := &api.InputRoomEventsResponse{} + if err = r.RSAPI.InputRoomEvents(context.Background(), inputReq, inputRes); err != nil { + return nil, fmt.Errorf("r.InputRoomEvents: %w", err) + } + } else { + // The invite originated over federation. Process the membership + // update, which will notify the sync API etc about the incoming + // invite. + updater, err := r.DB.MembershipUpdater(ctx, roomID, targetUserID, isTargetLocal, req.RoomVersion) + if err != nil { + return nil, fmt.Errorf("r.DB.MembershipUpdater: %w", err) + } + + unwrapped := event.Unwrap() + outputUpdates, err := helpers.UpdateToInviteMembership(updater, &unwrapped, nil, req.Event.RoomVersion) + if err != nil { + return nil, fmt.Errorf("updateToInviteMembership: %w", err) + } + + if err = updater.Commit(); err != nil { + return nil, fmt.Errorf("updater.Commit: %w", err) + } + + return outputUpdates, nil + } + + return nil, nil +} + +func buildInviteStrippedState( + ctx context.Context, + db storage.Database, + info *types.RoomInfo, + input *api.PerformInviteRequest, +) ([]gomatrixserverlib.InviteV2StrippedState, error) { + stateWanted := []gomatrixserverlib.StateKeyTuple{} + // "If they are set on the room, at least the state for m.room.avatar, m.room.canonical_alias, m.room.join_rules, and m.room.name SHOULD be included." + // https://matrix.org/docs/spec/client_server/r0.6.0#m-room-member + for _, t := range []string{ + gomatrixserverlib.MRoomName, gomatrixserverlib.MRoomCanonicalAlias, + gomatrixserverlib.MRoomAliases, gomatrixserverlib.MRoomJoinRules, + "m.room.avatar", "m.room.encryption", + } { + stateWanted = append(stateWanted, gomatrixserverlib.StateKeyTuple{ + EventType: t, + StateKey: "", + }) + } + roomState := state.NewStateResolution(db, *info) + stateEntries, err := roomState.LoadStateAtSnapshotForStringTuples( + ctx, info.StateSnapshotNID, stateWanted, + ) + if err != nil { + return nil, err + } + stateNIDs := []types.EventNID{} + for _, stateNID := range stateEntries { + stateNIDs = append(stateNIDs, stateNID.EventNID) + } + stateEvents, err := db.Events(ctx, stateNIDs) + if err != nil { + return nil, err + } + inviteState := []gomatrixserverlib.InviteV2StrippedState{ + gomatrixserverlib.NewInviteV2StrippedState(&input.Event.Event), + } + stateEvents = append(stateEvents, types.Event{Event: input.Event.Unwrap()}) + for _, event := range stateEvents { + inviteState = append(inviteState, gomatrixserverlib.NewInviteV2StrippedState(&event.Event)) + } + return inviteState, nil +} |