diff --git a/server/channels/app/platform/shared_channel_notifier.go b/server/channels/app/platform/shared_channel_notifier.go index b5cbe3919f..e99bbe365a 100644 --- a/server/channels/app/platform/shared_channel_notifier.go +++ b/server/channels/app/platform/shared_channel_notifier.go @@ -145,7 +145,7 @@ func handleInvitation(ps *PlatformService, syncService SharedChannelServiceIFace return errors.Wrap(err, fmt.Sprintf("couldn't find remote cluster %s, for creating shared channel invitation for a DM", *participant.RemoteId)) } - return syncService.SendChannelInvite(channel, creator.Id, rc, sharedchannel.WithDirectParticipantID(creator.Id), sharedchannel.WithDirectParticipantID(participant.Id)) + return syncService.SendChannelInvite(channel, creator.Id, rc, sharedchannel.WithDirectParticipant(creator), sharedchannel.WithDirectParticipant(participant)) } func getUserFromEvent(ps *PlatformService, event *model.WebSocketEvent, key string) (*model.User, error) { diff --git a/server/platform/services/sharedchannel/channelinvite.go b/server/platform/services/sharedchannel/channelinvite.go index ff0e016ec4..3adaf0f751 100644 --- a/server/platform/services/sharedchannel/channelinvite.go +++ b/server/platform/services/sharedchannel/channelinvite.go @@ -28,13 +28,26 @@ type channelInviteMsg struct { Purpose string `json:"purpose"` Type model.ChannelType `json:"type"` DirectParticipantIDs []string `json:"direct_participant_ids"` + DirectParticipants []*model.User `json:"direct_participants"` +} + +func (cim channelInviteMsg) DirectParticipantsMap() map[string]*model.User { + dim := make(map[string]*model.User) + for _, user := range cim.DirectParticipants { + dim[user.Id] = user + } + return dim } type InviteOption func(msg *channelInviteMsg) -func WithDirectParticipantID(participantID string) InviteOption { +func WithDirectParticipant(participant *model.User) InviteOption { return func(msg *channelInviteMsg) { - msg.DirectParticipantIDs = append(msg.DirectParticipantIDs, participantID) + msg.DirectParticipantIDs = append(msg.DirectParticipantIDs, participant.Id) + // if the participant is local, send it as part of the invite payload + if !participant.IsRemote() { + msg.DirectParticipants = append(msg.DirectParticipants, sanitizeUserForSync(participant)) + } } } @@ -355,6 +368,32 @@ func (scs *Service) handleChannelCreation(invite channelInviteMsg, rc *model.Rem return channel, true, nil } +// getOrCreateUser will try to fetch a user by its ID from the +// database and if it fails, it will try to create it if is present in +// the participantsMap +func (scs *Service) getOrCreateUser(userID string, participantsMap map[string]*model.User, rc *model.RemoteCluster) (*model.User, error) { + user, err := scs.server.GetStore().User().Get(context.TODO(), userID) + if err == nil { + return user, nil + } + + inviteUser, ok := participantsMap[userID] + if !ok { + // at this point we couldn't fetch the user nor we can create + // it from the invite information, so we return an error + return nil, fmt.Errorf("cannot fetch user `%q`: %w", userID, err) + } + + var rctx request.CTX = request.EmptyContext(scs.server.Log()) + inviteUser.RemoteId = model.NewPointer(rc.RemoteId) + user, iErr := scs.insertSyncUser(rctx, inviteUser, nil, rc) + if iErr != nil { + return nil, fmt.Errorf("cannot create user `%q` for remote `%q`: %w", inviteUser.Id, rc.RemoteId, iErr) + } + + return user, nil +} + // createDirectChannel creates a DM channel, or fetches an existing channel, and returns the channel plus a boolean // indicating if the channel is new. func (scs *Service) createDirectChannel(invite channelInviteMsg, rc *model.RemoteCluster) (*model.Channel, bool, error) { @@ -362,14 +401,16 @@ func (scs *Service) createDirectChannel(invite channelInviteMsg, rc *model.Remot return nil, false, fmt.Errorf("cannot create direct channel `%s` insufficient participant count `%d`", invite.ChannelId, len(invite.DirectParticipantIDs)) } - user1, err := scs.server.GetStore().User().Get(context.TODO(), invite.DirectParticipantIDs[0]) + participantsMap := invite.DirectParticipantsMap() + + user1, err := scs.getOrCreateUser(invite.DirectParticipantIDs[0], participantsMap, rc) if err != nil { - return nil, false, fmt.Errorf("cannot create direct channel `%s` cannot fetch user1 (%s): %w", invite.ChannelId, invite.DirectParticipantIDs[0], err) + return nil, false, fmt.Errorf("cannot create direct channel `%s` from invite: %w", invite.ChannelId, err) } - user2, err := scs.server.GetStore().User().Get(context.TODO(), invite.DirectParticipantIDs[1]) + user2, err := scs.getOrCreateUser(invite.DirectParticipantIDs[1], participantsMap, rc) if err != nil { - return nil, false, fmt.Errorf("cannot create direct channel `%s` cannot fetch user2 (%s): %w", invite.ChannelId, invite.DirectParticipantIDs[1], err) + return nil, false, fmt.Errorf("cannot create direct channel `%s` from invite: %w", invite.ChannelId, err) } // determine the remote user diff --git a/server/platform/services/sharedchannel/channelinvite_test.go b/server/platform/services/sharedchannel/channelinvite_test.go index d9c271822d..8f0f94f69c 100644 --- a/server/platform/services/sharedchannel/channelinvite_test.go +++ b/server/platform/services/sharedchannel/channelinvite_test.go @@ -23,6 +23,7 @@ import ( var ( mockTypeChannel = mock.AnythingOfType("*model.Channel") + mockTypeUser = mock.AnythingOfType("*model.User") mockTypeString = mock.AnythingOfType("string") mockTypeReqContext = mock.AnythingOfType("*request.Context") mockTypeContext = mock.MatchedBy(func(ctx context.Context) bool { return true }) @@ -264,18 +265,22 @@ func TestOnReceiveChannelInvite(t *testing.T) { t.Run("DM channels", func(t *testing.T) { var testRemoteID = model.NewId() testCases := []struct { - desc string - user1 *model.User - user2 *model.User - canSee bool - expectSuccess bool + desc string + user1 *model.User + user2 *model.User + canSee bool + expectSuccess bool + user2InDB bool + user2InParticipants bool }{ - {"valid users", &model.User{Id: model.NewId(), RemoteId: &testRemoteID}, &model.User{Id: model.NewId()}, true, true}, - {"swapped users", &model.User{Id: model.NewId()}, &model.User{Id: model.NewId(), RemoteId: &testRemoteID}, true, true}, - {"two remotes", &model.User{Id: model.NewId(), RemoteId: &testRemoteID}, &model.User{Id: model.NewId(), RemoteId: &testRemoteID}, true, false}, - {"two locals", &model.User{Id: model.NewId()}, &model.User{Id: model.NewId()}, true, false}, - {"can't see", &model.User{Id: model.NewId(), RemoteId: &testRemoteID}, &model.User{Id: model.NewId()}, false, false}, - {"invalid remoteid", &model.User{Id: model.NewId(), RemoteId: model.NewPointer("bogus")}, &model.User{Id: model.NewId()}, true, false}, + {"valid users", &model.User{Id: model.NewId(), RemoteId: &testRemoteID}, &model.User{Id: model.NewId()}, true, true, true, false}, + {"swapped users", &model.User{Id: model.NewId()}, &model.User{Id: model.NewId(), RemoteId: &testRemoteID}, true, true, true, false}, + {"two remotes", &model.User{Id: model.NewId(), RemoteId: &testRemoteID}, &model.User{Id: model.NewId(), RemoteId: &testRemoteID}, true, false, true, false}, + {"two locals", &model.User{Id: model.NewId()}, &model.User{Id: model.NewId()}, true, false, true, false}, + {"can't see", &model.User{Id: model.NewId(), RemoteId: &testRemoteID}, &model.User{Id: model.NewId()}, false, false, true, false}, + {"invalid remoteid", &model.User{Id: model.NewId(), RemoteId: model.NewPointer("bogus")}, &model.User{Id: model.NewId()}, true, false, true, false}, + {"user2 not in DB but in participants", &model.User{Id: model.NewId(), RemoteId: &testRemoteID}, &model.User{Id: model.NewId()}, true, true, false, true}, + {"user2 not in DB and not in participants", &model.User{Id: model.NewId(), RemoteId: &testRemoteID}, &model.User{Id: model.NewId()}, true, false, false, false}, } for _, tc := range testCases { @@ -298,6 +303,12 @@ func TestOnReceiveChannelInvite(t *testing.T) { Type: model.ChannelTypeDirect, DirectParticipantIDs: []string{tc.user1.Id, tc.user2.Id}, } + + // Add participants to the invitation if needed + if tc.user2InParticipants { + invitation.DirectParticipants = append(invitation.DirectParticipants, tc.user2) + } + payload, err := json.Marshal(invitation) require.NoError(t, err) @@ -313,8 +324,20 @@ func TestOnReceiveChannelInvite(t *testing.T) { mockUserStore := mocks.UserStore{} mockUserStore.On("Get", mockTypeContext, tc.user1.Id). Return(tc.user1, nil) - mockUserStore.On("Get", mockTypeContext, tc.user2.Id). - Return(tc.user2, nil) + if tc.user2InDB { + mockUserStore.On("Get", mockTypeContext, tc.user2.Id). + Return(tc.user2, nil) + } else { + mockUserStore.On("Get", mockTypeContext, tc.user2.Id). + Return(nil, &store.ErrNotFound{}) + } + + if tc.user2InParticipants { + mockUserStore.On("Save", mock.AnythingOfType("*request.Context"), + mock.MatchedBy(func(u *model.User) bool { + return u.Id == tc.user2.Id + })).Return(tc.user2, nil) + } mockChannelStore.On("Get", invitation.ChannelId, true).Return(nil, errors.New("boom")) mockChannelStore.On("GetByName", "", mockTypeString, true).Return(nil, &store.ErrNotFound{}) @@ -332,6 +355,7 @@ func TestOnReceiveChannelInvite(t *testing.T) { mockApp.On("GetOrCreateDirectChannel", mockTypeReqContext, mockTypeString, mockTypeString, mock.AnythingOfType("model.ChannelOption")). Return(channel, nil).Maybe() mockApp.On("UserCanSeeOtherUser", mockTypeReqContext, mockTypeString, mockTypeString).Return(tc.canSee, nil).Maybe() + mockApp.On("NotifySharedChannelUserUpdate", mockTypeUser).Return().Maybe() defer mockApp.AssertExpectations(t)