diff --git a/server/channels/app/channel.go b/server/channels/app/channel.go index 80c04f87ef..328e1c43c4 100644 --- a/server/channels/app/channel.go +++ b/server/channels/app/channel.go @@ -13,6 +13,7 @@ import ( "strings" "github.com/mattermost/mattermost/server/v8/channels/utils" + "github.com/mattermost/mattermost/server/v8/platform/services/sharedchannel" "github.com/mattermost/mattermost/server/v8/platform/services/telemetry" "github.com/mattermost/mattermost/server/public/model" @@ -503,8 +504,8 @@ func (a *App) createDirectChannelWithUser(c request.CTX, user, otherUser *model. return channel, nil } -func (a *App) CreateGroupChannel(c request.CTX, userIDs []string, creatorId string) (*model.Channel, *model.AppError) { - channel, err := a.createGroupChannel(c, userIDs) +func (a *App) CreateGroupChannel(c request.CTX, userIDs []string, creatorId string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) { + channel, err := a.createGroupChannel(c, userIDs, creatorId, channelOptions...) if err != nil { if err.Id == store.ChannelExistsError { return channel, nil @@ -524,7 +525,11 @@ func (a *App) CreateGroupChannel(c request.CTX, userIDs []string, creatorId stri return channel, nil } -func (a *App) createGroupChannel(c request.CTX, userIDs []string) (*model.Channel, *model.AppError) { +// creatorId is used to determine if the group channel should have a +// shared channel record attached. It can be empty if the caller +// doesn't know who the creator is (e.g. the import process) and the +// resulting group channel will not be shared +func (a *App) createGroupChannel(c request.CTX, userIDs []string, creatorID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) { if len(userIDs) > model.ChannelGroupMaxUsers || len(userIDs) < model.ChannelGroupMinUsers { return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest) } @@ -538,7 +543,21 @@ func (a *App) createGroupChannel(c request.CTX, userIDs []string) (*model.Channe return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_user.app_error", nil, "user_ids="+model.ArrayToJSON(userIDs), http.StatusBadRequest) } - if !a.Config().FeatureFlags.EnableSharedChannelsDMs { + // extracts the creator and the remotes involved in the GM to + // decide how to handle the shared part of the creation + var creator *model.User + remoteIDs := map[string]bool{} + for _, user := range users { + if user.Id == creatorID { + creator = user + } + if user.IsRemote() { + remoteIDs[*user.RemoteId] = true + } + } + channelIsShared := len(remoteIDs) > 0 + + if channelIsShared && !a.Config().FeatureFlags.EnableSharedChannelsDMs { for _, user := range users { if user.IsRemote() { return nil, model.NewAppError("createGroupChannel", "api.channel.create_group.remote_restricted.app_error", nil, "", http.StatusForbidden) @@ -550,9 +569,10 @@ func (a *App) createGroupChannel(c request.CTX, userIDs []string) (*model.Channe Name: model.GetGroupNameFromUserIds(userIDs), DisplayName: model.GetGroupDisplayNameFromUsers(users, true), Type: model.ChannelTypeGroup, + Shared: model.NewPointer(channelIsShared), } - channel, nErr := a.Srv().Store().Channel().Save(c, group, *a.Config().TeamSettings.MaxChannelsPerTeam) + channel, nErr := a.Srv().Store().Channel().Save(c, group, *a.Config().TeamSettings.MaxChannelsPerTeam, channelOptions...) if nErr != nil { var invErr *store.ErrInvalidInput var cErr *store.ErrConflict @@ -608,6 +628,48 @@ func (a *App) createGroupChannel(c request.CTX, userIDs []string) (*model.Channe } } + // When the newly created channel is shared, the creator is local + // and one of the participants is remote create a local shared + // channel record + if channel.IsShared() && creator != nil && !creator.IsRemote() { + sc := &model.SharedChannel{ + ChannelId: channel.Id, + TeamId: channel.TeamId, + Home: true, + ReadOnly: false, + ShareName: channel.Name, + ShareDisplayName: channel.DisplayName, + SharePurpose: channel.Purpose, + ShareHeader: channel.Header, + CreatorId: creatorID, + Type: channel.Type, + } + + if _, err := a.ShareChannel(c, sc); err != nil { + c.Logger().Error("Failed to share newly created group channel", mlog.String("channel_id", channel.Id), mlog.Err(err)) + } else { + // if we could successfully share the channel, we invite + // the remotes involved to it + if sc, _ := a.getSharedChannelsService(); sc != nil { + for remoteID := range remoteIDs { + rc, err := a.Srv().Store().RemoteCluster().Get(remoteID, false) + if err != nil { + c.Logger().Error("Failed to send invite to group message channel, can't retrieve remote cluster", mlog.String("channel_id", channel.Id), mlog.String("remote_id", remoteID), mlog.Err(err)) + continue + } + + opts := []sharedchannel.InviteOption{sharedchannel.WithCreator(creatorID)} + for _, user := range users { + opts = append(opts, sharedchannel.WithDirectParticipant(user, remoteID)) + } + if err := sc.SendChannelInvite(channel, creatorID, rc, opts...); err != nil { + c.Logger().Error("Failed to send invite to group message channel, error sending the invite", mlog.String("channel_id", channel.Id), mlog.String("remote_id", remoteID), mlog.Err(err)) + } + } + } + } + } + a.Srv().Go(func() { pluginContext := pluginContext(c) a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool { diff --git a/server/channels/app/import_functions.go b/server/channels/app/import_functions.go index 55fcc00043..19fad5e847 100644 --- a/server/channels/app/import_functions.go +++ b/server/channels/app/import_functions.go @@ -2105,7 +2105,7 @@ func (a *App) importDirectChannel(rctx request.CTX, data *imports.DirectChannelI } channel = ch } else { - ch, err2 := a.createGroupChannel(rctx, userIDs) + ch, err2 := a.createGroupChannel(rctx, userIDs, "") if err2 != nil && err2.Id != store.ChannelExistsError { return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_group_channel.error", nil, "", http.StatusBadRequest).Wrap(err2) } @@ -2350,7 +2350,7 @@ func (a *App) importMultipleDirectPostLines(rctx request.CTX, lines []imports.Li } channel = ch } else if len(userIDs) > 2 { - ch, err = a.createGroupChannel(rctx, userIDs) + ch, err = a.createGroupChannel(rctx, userIDs, "") if err != nil && err.Id != store.ChannelExistsError { return line.LineNumber, model.NewAppError("BulkImport", "app.import.import_direct_post.create_group_channel.error", nil, "", http.StatusBadRequest).Wrap(err) } diff --git a/server/channels/app/import_functions_test.go b/server/channels/app/import_functions_test.go index 6332be9788..60c06e5365 100644 --- a/server/channels/app/import_functions_test.go +++ b/server/channels/app/import_functions_test.go @@ -4050,7 +4050,7 @@ func TestImportImportDirectChannel(t *testing.T) { th.BasicUser2.Id, user3.Id, } - channel, appErr := th.App.createGroupChannel(th.Context, userIDs) + channel, appErr := th.App.createGroupChannel(th.Context, userIDs, th.BasicUser.Id) require.Equal(t, appErr.Id, store.ChannelExistsError) require.Equal(t, channel.Header, *data.Header) }) @@ -4677,7 +4677,7 @@ func TestImportImportDirectPost(t *testing.T) { th.BasicUser2.Id, user3.Id, } - channel, appErr = th.App.createGroupChannel(th.Context, userIDs) + channel, appErr = th.App.createGroupChannel(th.Context, userIDs, th.BasicUser.Id) require.Equal(t, appErr.Id, store.ChannelExistsError) groupChannel = channel diff --git a/server/channels/app/platform/shared_channel_notifier.go b/server/channels/app/platform/shared_channel_notifier.go index e99bbe365a..84764748bc 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.WithDirectParticipant(creator), sharedchannel.WithDirectParticipant(participant)) + return syncService.SendChannelInvite(channel, creator.Id, rc, sharedchannel.WithDirectParticipant(creator, rc.RemoteId), sharedchannel.WithDirectParticipant(participant, rc.RemoteId)) } func getUserFromEvent(ps *PlatformService, event *model.WebSocketEvent, key string) (*model.User, error) { diff --git a/server/channels/app/post_metadata_test.go b/server/channels/app/post_metadata_test.go index dfee7d272f..0e335df363 100644 --- a/server/channels/app/post_metadata_test.go +++ b/server/channels/app/post_metadata_test.go @@ -681,7 +681,7 @@ func TestPreparePostForClient(t *testing.T) { directChannel, err := th.App.createDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id) require.Nil(t, err) - groupChannel, err := th.App.createGroupChannel(th.Context, []string{th.BasicUser.Id, th.BasicUser2.Id, th.CreateUser().Id}) + groupChannel, err := th.App.createGroupChannel(th.Context, []string{th.BasicUser.Id, th.BasicUser2.Id, th.CreateUser().Id}, th.BasicUser.Id) require.Nil(t, err) testCases := []struct { diff --git a/server/channels/app/post_test.go b/server/channels/app/post_test.go index 1e7fc9818e..f4c3b0277b 100644 --- a/server/channels/app/post_test.go +++ b/server/channels/app/post_test.go @@ -1162,7 +1162,7 @@ func TestCreatePost(t *testing.T) { user1 := th.CreateUser() user2 := th.CreateUser() user3 := th.CreateUser() - gm, appErr := th.App.createGroupChannel(th.Context, []string{user1.Id, user2.Id, user3.Id}) + gm, appErr := th.App.createGroupChannel(th.Context, []string{user1.Id, user2.Id, user3.Id}, user1.Id) require.Nil(t, appErr) require.NotNil(t, gm) @@ -2423,7 +2423,7 @@ func TestCountMentionsFromPost(t *testing.T) { user2 := th.BasicUser2 user3 := th.SystemAdminUser - channel, err := th.App.createGroupChannel(th.Context, []string{user1.Id, user2.Id, user3.Id}) + channel, err := th.App.createGroupChannel(th.Context, []string{user1.Id, user2.Id, user3.Id}, user1.Id) require.Nil(t, err) post1, err := th.App.CreatePost(th.Context, &model.Post{ diff --git a/server/channels/store/retrylayer/retrylayer.go b/server/channels/store/retrylayer/retrylayer.go index 2fc01b7632..e80e83c994 100644 --- a/server/channels/store/retrylayer/retrylayer.go +++ b/server/channels/store/retrylayer/retrylayer.go @@ -2858,11 +2858,11 @@ func (s *RetryLayerChannelStore) Restore(channelID string, timestamp int64) erro } -func (s *RetryLayerChannelStore) Save(rctx request.CTX, channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) { +func (s *RetryLayerChannelStore) Save(rctx request.CTX, channel *model.Channel, maxChannelsPerTeam int64, channelOptions ...model.ChannelOption) (*model.Channel, error) { tries := 0 for { - result, err := s.ChannelStore.Save(rctx, channel, maxChannelsPerTeam) + result, err := s.ChannelStore.Save(rctx, channel, maxChannelsPerTeam, channelOptions...) if err == nil { return result, nil } diff --git a/server/channels/store/searchlayer/channel_layer.go b/server/channels/store/searchlayer/channel_layer.go index b56541e345..60db3a8d32 100644 --- a/server/channels/store/searchlayer/channel_layer.go +++ b/server/channels/store/searchlayer/channel_layer.go @@ -66,8 +66,8 @@ func (c *SearchChannelStore) indexChannel(rctx request.CTX, channel *model.Chann } } -func (c *SearchChannelStore) Save(rctx request.CTX, channel *model.Channel, maxChannels int64) (*model.Channel, error) { - newChannel, err := c.ChannelStore.Save(rctx, channel, maxChannels) +func (c *SearchChannelStore) Save(rctx request.CTX, channel *model.Channel, maxChannels int64, channelOptions ...model.ChannelOption) (*model.Channel, error) { + newChannel, err := c.ChannelStore.Save(rctx, channel, maxChannels, channelOptions...) if err == nil { c.indexChannel(rctx, newChannel) } diff --git a/server/channels/store/sqlstore/channel_store.go b/server/channels/store/sqlstore/channel_store.go index e34998a11b..c12fb586c0 100644 --- a/server/channels/store/sqlstore/channel_store.go +++ b/server/channels/store/sqlstore/channel_store.go @@ -616,7 +616,11 @@ func (s SqlChannelStore) upsertPublicChannelT(transaction *sqlxTxWrapper, channe } // Save writes the (non-direct) channel to the database. -func (s SqlChannelStore) Save(rctx request.CTX, channel *model.Channel, maxChannelsPerTeam int64) (_ *model.Channel, err error) { +func (s SqlChannelStore) Save(rctx request.CTX, channel *model.Channel, maxChannelsPerTeam int64, channelOptions ...model.ChannelOption) (_ *model.Channel, err error) { + for _, option := range channelOptions { + option(channel) + } + if channel.DeleteAt != 0 { return nil, store.NewErrInvalidInput("Channel", "DeleteAt", channel.DeleteAt) } diff --git a/server/channels/store/store.go b/server/channels/store/store.go index cbb31bc82d..b33fa2f0e5 100644 --- a/server/channels/store/store.go +++ b/server/channels/store/store.go @@ -188,7 +188,7 @@ type TeamStore interface { } type ChannelStore interface { - Save(rctx request.CTX, channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) + Save(rctx request.CTX, channel *model.Channel, maxChannelsPerTeam int64, channelOptions ...model.ChannelOption) (*model.Channel, error) CreateDirectChannel(ctx request.CTX, userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) SaveDirectChannel(ctx request.CTX, channel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error) Update(ctx request.CTX, channel *model.Channel) (*model.Channel, error) diff --git a/server/channels/store/storetest/mocks/ChannelStore.go b/server/channels/store/storetest/mocks/ChannelStore.go index fd2ab03d3d..4e2c7f3026 100644 --- a/server/channels/store/storetest/mocks/ChannelStore.go +++ b/server/channels/store/storetest/mocks/ChannelStore.go @@ -2537,9 +2537,16 @@ func (_m *ChannelStore) Restore(channelID string, timestamp int64) error { return r0 } -// Save provides a mock function with given fields: rctx, channel, maxChannelsPerTeam -func (_m *ChannelStore) Save(rctx request.CTX, channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) { - ret := _m.Called(rctx, channel, maxChannelsPerTeam) +// Save provides a mock function with given fields: rctx, channel, maxChannelsPerTeam, channelOptions +func (_m *ChannelStore) Save(rctx request.CTX, channel *model.Channel, maxChannelsPerTeam int64, channelOptions ...model.ChannelOption) (*model.Channel, error) { + _va := make([]interface{}, len(channelOptions)) + for _i := range channelOptions { + _va[_i] = channelOptions[_i] + } + var _ca []interface{} + _ca = append(_ca, rctx, channel, maxChannelsPerTeam) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) if len(ret) == 0 { panic("no return value specified for Save") @@ -2547,19 +2554,19 @@ func (_m *ChannelStore) Save(rctx request.CTX, channel *model.Channel, maxChanne var r0 *model.Channel var r1 error - if rf, ok := ret.Get(0).(func(request.CTX, *model.Channel, int64) (*model.Channel, error)); ok { - return rf(rctx, channel, maxChannelsPerTeam) + if rf, ok := ret.Get(0).(func(request.CTX, *model.Channel, int64, ...model.ChannelOption) (*model.Channel, error)); ok { + return rf(rctx, channel, maxChannelsPerTeam, channelOptions...) } - if rf, ok := ret.Get(0).(func(request.CTX, *model.Channel, int64) *model.Channel); ok { - r0 = rf(rctx, channel, maxChannelsPerTeam) + if rf, ok := ret.Get(0).(func(request.CTX, *model.Channel, int64, ...model.ChannelOption) *model.Channel); ok { + r0 = rf(rctx, channel, maxChannelsPerTeam, channelOptions...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Channel) } } - if rf, ok := ret.Get(1).(func(request.CTX, *model.Channel, int64) error); ok { - r1 = rf(rctx, channel, maxChannelsPerTeam) + if rf, ok := ret.Get(1).(func(request.CTX, *model.Channel, int64, ...model.ChannelOption) error); ok { + r1 = rf(rctx, channel, maxChannelsPerTeam, channelOptions...) } else { r1 = ret.Error(1) } diff --git a/server/channels/store/timerlayer/timerlayer.go b/server/channels/store/timerlayer/timerlayer.go index 42cad90e3a..884b70c2e9 100644 --- a/server/channels/store/timerlayer/timerlayer.go +++ b/server/channels/store/timerlayer/timerlayer.go @@ -2394,10 +2394,10 @@ func (s *TimerLayerChannelStore) Restore(channelID string, timestamp int64) erro return err } -func (s *TimerLayerChannelStore) Save(rctx request.CTX, channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) { +func (s *TimerLayerChannelStore) Save(rctx request.CTX, channel *model.Channel, maxChannelsPerTeam int64, channelOptions ...model.ChannelOption) (*model.Channel, error) { start := time.Now() - result, err := s.ChannelStore.Save(rctx, channel, maxChannelsPerTeam) + result, err := s.ChannelStore.Save(rctx, channel, maxChannelsPerTeam, channelOptions...) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { diff --git a/server/platform/services/sharedchannel/channelinvite.go b/server/platform/services/sharedchannel/channelinvite.go index 3adaf0f751..363367aeb0 100644 --- a/server/platform/services/sharedchannel/channelinvite.go +++ b/server/platform/services/sharedchannel/channelinvite.go @@ -27,6 +27,7 @@ type channelInviteMsg struct { Header string `json:"header"` Purpose string `json:"purpose"` Type model.ChannelType `json:"type"` + CreatorID string `json:"creator_id"` DirectParticipantIDs []string `json:"direct_participant_ids"` DirectParticipants []*model.User `json:"direct_participants"` } @@ -41,16 +42,24 @@ func (cim channelInviteMsg) DirectParticipantsMap() map[string]*model.User { type InviteOption func(msg *channelInviteMsg) -func WithDirectParticipant(participant *model.User) InviteOption { +func WithDirectParticipant(participant *model.User, remoteID string) InviteOption { return func(msg *channelInviteMsg) { msg.DirectParticipantIDs = append(msg.DirectParticipantIDs, participant.Id) - // if the participant is local, send it as part of the invite payload - if !participant.IsRemote() { + // if the participant doesn't belong to the remote we're + // sending the invite to, send it as part of the invite + // payload + if participant.GetRemoteID() != remoteID { msg.DirectParticipants = append(msg.DirectParticipants, sanitizeUserForSync(participant)) } } } +func WithCreator(creatorID string) InviteOption { + return func(msg *channelInviteMsg) { + msg.CreatorID = creatorID + } +} + // SendChannelInvite asynchronously sends a channel invite to a remote cluster. The remote cluster is // expected to create a new channel with the same channel id, and respond with status OK. // If an error occurs on the remote cluster then an ephemeral message is posted to in the channel for userId. @@ -241,9 +250,9 @@ func (scs *Service) onReceiveChannelInvite(msg model.RemoteClusterMsg, rc *model // sanity check to ensure the channel returned has the expected id. Otherwise sync will not work as expected and will fail // silently. if invite.ChannelId != channel.Id { - // as of this writing, this scenario should only be possible if the invite included a DM channel invitation with a - // combination of two user ids (one remote, one local) that already have a DM on this server. Very unlikely unless - // the remote is compromised AND has knowledge of the local user id. + // as of this writing, this scenario should only be possible if the invite included a DM or GM channel + // invitation with a combination of user ids that already have a DM or GM on this server. Very unlikely + // unless the remote is compromised AND has knowledge of the local user ids. // Another possibility would be an actual user ID collision between two servers, where the likelihood is // infinitesimally small scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Channel invite failed - channel created/fetched with wrong id", @@ -254,7 +263,7 @@ func (scs *Service) onReceiveChannelInvite(msg model.RemoteClusterMsg, rc *model mlog.String("team_id", invite.TeamId), mlog.Array("dm_partics", invite.DirectParticipantIDs), ) - return fmt.Errorf("cannot create shared channel (DM channel_id=%s): %w", invite.ChannelId, model.ErrChannelAlreadyExists) + return fmt.Errorf("cannot create shared channel (channel_id=%s channel_type=%s): %w", invite.ChannelId, invite.Type, model.ErrChannelAlreadyExists) } // mark the newly created channel read-only if requested in the invite @@ -303,10 +312,14 @@ func (scs *Service) onReceiveChannelInvite(msg model.RemoteClusterMsg, rc *model return fmt.Errorf("cannot restore deleted shared channel remote (channel_id=%s): %w", invite.ChannelId, err) } } else { + creatorID := channel.CreatorId + if creatorID == "" { + creatorID = invite.CreatorID + } scr := &model.SharedChannelRemote{ Id: model.NewId(), ChannelId: channel.Id, - CreatorId: channel.CreatorId, + CreatorId: creatorID, IsInviteAccepted: true, IsInviteConfirmed: true, RemoteId: rc.RemoteId, @@ -335,6 +348,10 @@ func (scs *Service) handleChannelCreation(invite channelInviteMsg, rc *model.Rem return scs.createDirectChannel(invite, rc) } + if invite.Type == model.ChannelTypeGroup { + return scs.createGroupChannel(invite, rc) + } + teamId := rc.DefaultTeamId // if the remote doesn't have a teamId associated and until the // acceptance of an invite includes selecting a team, we use the @@ -472,3 +489,62 @@ func (scs *Service) createDirectChannel(invite channelInviteMsg, rc *model.Remot return channel, true, nil } + +// createGroupChannel 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) createGroupChannel(invite channelInviteMsg, rc *model.RemoteCluster) (*model.Channel, bool, error) { + if len(invite.DirectParticipantIDs) > model.ChannelGroupMaxUsers || len(invite.DirectParticipantIDs) < model.ChannelGroupMinUsers { + return nil, false, fmt.Errorf("cannot create group channel `%s` bad participant count `%d`", invite.ChannelId, len(invite.DirectParticipantIDs)) + } + + participantsMap := invite.DirectParticipantsMap() + + remoteIDMap := map[string]bool{} + hasLocalUsers := false + for _, participantID := range invite.DirectParticipantIDs { + user, err := scs.getOrCreateUser(participantID, participantsMap, rc) + if err != nil { + return nil, false, fmt.Errorf("cannot create group channel `%s` from invite: %w", invite.ChannelId, err) + } + + // we keep track of the origin of the users to check if the + // invite is valid + if user.IsRemote() { + remoteIDMap[user.GetRemoteID()] = true + } else { + hasLocalUsers = true + } + } + + // if the invite doesn't contain remote users, GM should not be created via remote invite + if len(remoteIDMap) == 0 { + return nil, false, fmt.Errorf("cannot create group channel `%s` there are no remote users", invite.ChannelId) + } + + // if the channel doesn't contain local users, the GM channel doesn't belong to this server + if !hasLocalUsers { + return nil, false, fmt.Errorf("cannot create group channel `%s` there are no local users", invite.ChannelId) + } + + // check if this DM already exists. + channelName := model.GetGroupNameFromUserIds(invite.DirectParticipantIDs) + channelExists, err := scs.server.GetStore().Channel().GetByName("", channelName, true) + if err != nil && !isNotFoundError(err) { + return nil, false, fmt.Errorf("cannot check GM channel exists (%s): %w", channelName, err) + } + if channelExists != nil { + if channelExists.Id == invite.ChannelId { + return channelExists, false, nil + } + + return nil, false, fmt.Errorf("cannot create group channel `%s`: channel exists with wrong id", channelName) + } + + // create the channel + channel, appErr := scs.app.CreateGroupChannel(request.EmptyContext(scs.server.Log()), invite.DirectParticipantIDs, invite.CreatorID, model.WithID(invite.ChannelId)) + if appErr != nil { + return nil, false, fmt.Errorf("cannot create group channel `%s`: %w", invite.ChannelId, appErr) + } + + return channel, true, nil +} diff --git a/server/platform/services/sharedchannel/mock_AppIface_test.go b/server/platform/services/sharedchannel/mock_AppIface_test.go index a10449fa41..9a3105e105 100644 --- a/server/platform/services/sharedchannel/mock_AppIface_test.go +++ b/server/platform/services/sharedchannel/mock_AppIface_test.go @@ -102,6 +102,45 @@ func (_m *MockAppIface) CreateChannelWithUser(c request.CTX, channel *model.Chan return r0, r1 } +// CreateGroupChannel provides a mock function with given fields: c, userIDs, creatorId, channelOptions +func (_m *MockAppIface) CreateGroupChannel(c request.CTX, userIDs []string, creatorId string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) { + _va := make([]interface{}, len(channelOptions)) + for _i := range channelOptions { + _va[_i] = channelOptions[_i] + } + var _ca []interface{} + _ca = append(_ca, c, userIDs, creatorId) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for CreateGroupChannel") + } + + var r0 *model.Channel + var r1 *model.AppError + if rf, ok := ret.Get(0).(func(request.CTX, []string, string, ...model.ChannelOption) (*model.Channel, *model.AppError)); ok { + return rf(c, userIDs, creatorId, channelOptions...) + } + if rf, ok := ret.Get(0).(func(request.CTX, []string, string, ...model.ChannelOption) *model.Channel); ok { + r0 = rf(c, userIDs, creatorId, channelOptions...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Channel) + } + } + + if rf, ok := ret.Get(1).(func(request.CTX, []string, string, ...model.ChannelOption) *model.AppError); ok { + r1 = rf(c, userIDs, creatorId, channelOptions...) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // CreatePost provides a mock function with given fields: c, post, channel, flags func (_m *MockAppIface) CreatePost(c request.CTX, post *model.Post, channel *model.Channel, flags model.CreatePostFlags) (*model.Post, *model.AppError) { ret := _m.Called(c, post, channel, flags) diff --git a/server/platform/services/sharedchannel/service.go b/server/platform/services/sharedchannel/service.go index 1d2c182fea..7d9cbe364f 100644 --- a/server/platform/services/sharedchannel/service.go +++ b/server/platform/services/sharedchannel/service.go @@ -53,6 +53,7 @@ type AppIface interface { SendEphemeralPost(c request.CTX, userId string, post *model.Post) *model.Post CreateChannelWithUser(c request.CTX, channel *model.Channel, userId string) (*model.Channel, *model.AppError) GetOrCreateDirectChannel(c request.CTX, userId, otherUserId string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) + CreateGroupChannel(c request.CTX, userIDs []string, creatorId string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) UserCanSeeOtherUser(c request.CTX, userID string, otherUserId string) (bool, *model.AppError) AddUserToChannel(c request.CTX, user *model.User, channel *model.Channel, skipTeamMemberIntegrityCheck bool) (*model.ChannelMember, *model.AppError) AddUserToTeamByTeamId(c request.CTX, teamId string, user *model.User) *model.AppError diff --git a/server/platform/services/sharedchannel/sync_recv.go b/server/platform/services/sharedchannel/sync_recv.go index 7cb72e28a7..028c654283 100644 --- a/server/platform/services/sharedchannel/sync_recv.go +++ b/server/platform/services/sharedchannel/sync_recv.go @@ -115,7 +115,7 @@ func (scs *Service) processSyncMessage(c request.CTX, syncMsg *model.SyncMsg, rc continue } - if targetChannel.Type != model.ChannelTypeDirect && team == nil { + if (targetChannel.Type != model.ChannelTypeDirect && targetChannel.Type != model.ChannelTypeGroup) && team == nil { var err2 error team, err2 = scs.server.GetStore().Channel().GetTeamForChannel(syncMsg.ChannelId) if err2 != nil { diff --git a/server/platform/services/slackimport/slackimport.go b/server/platform/services/slackimport/slackimport.go index d39034e034..98cae638c0 100644 --- a/server/platform/services/slackimport/slackimport.go +++ b/server/platform/services/slackimport/slackimport.go @@ -88,7 +88,7 @@ type Actions struct { AddUserToChannel func(request.CTX, *model.User, *model.Channel, bool) (*model.ChannelMember, *model.AppError) JoinUserToTeam func(*model.Team, *model.User, string) (*model.TeamMember, *model.AppError) CreateDirectChannel func(request.CTX, string, string, ...model.ChannelOption) (*model.Channel, *model.AppError) - CreateGroupChannel func(request.CTX, []string) (*model.Channel, *model.AppError) + CreateGroupChannel func(request.CTX, []string, string, ...model.ChannelOption) (*model.Channel, *model.AppError) CreateChannel func(*model.Channel, bool) (*model.Channel, *model.AppError) DoUploadFile func(time.Time, string, string, string, string, []byte) (*model.FileInfo, *model.AppError) GenerateThumbnailImage func(request.CTX, image.Image, string, string) @@ -764,7 +764,7 @@ func (si *SlackImporter) oldImportChannel(rctx request.CTX, channel *model.Chann if creator == nil { return nil } - sc, err := si.actions.CreateGroupChannel(rctx, members) + sc, err := si.actions.CreateGroupChannel(rctx, members, "") if err != nil { return nil }