aboutsummaryrefslogtreecommitdiff
path: root/roomserver/internal/perform/perform_invite.go
diff options
context:
space:
mode:
Diffstat (limited to 'roomserver/internal/perform/perform_invite.go')
-rw-r--r--roomserver/internal/perform/perform_invite.go243
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
+}