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 удалений

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

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

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

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

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

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

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

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

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

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