Adds support for GMs in shared channels (#31403)

* Adds support for GMs in shared channels

* Fix linter

* Remove creatorID from slack call

---------

Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Этот коммит содержится в:
Miguel de la Cruz
2025-06-13 12:43:30 +02:00
коммит произвёл GitHub
родитель 07edaa875b
Коммит 43018759e5
17 изменённых файлов: 230 добавлений и 41 удалений

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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