Adds direct participants to the channel invite (#30404)

* Adds direct participants to the channel invite

The channel invite now contains the sanitized users that are local to
the node that is sending the invite. In the event that the receiving
server doesn't have those users in its local database, it can create
them from the invite and correctly generate the DM or GM with them as
members.

* Use IsRemote instead of directly checking user attributes

---------

Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Miguel de la Cruz
2025-04-10 19:19:59 +02:00
коммит произвёл GitHub
родитель 14426af461
Коммит 3ab0da1648
3 изменённых файлов: 85 добавлений и 20 удалений

Просмотреть файл

@@ -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) {

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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)