// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package sharedchannel import ( "errors" "fmt" "net/http" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/channels/store" ) // ShareChannel marks a local channel as shared. If the channel is already shared this method has // no effect and returns without error. // TeamId, type, displayname, purpose, and header are fetched from the channel if not provided. func (scs *Service) ShareChannel(sc *model.SharedChannel) (*model.SharedChannel, error) { channel, err := scs.server.GetStore().Channel().Get(sc.ChannelId, true) if err != nil { return nil, fmt.Errorf("cannot fetch channel while sharing channel %s: %w", sc.ChannelId, err) } if !scs.server.Config().FeatureFlags.EnableSharedChannelsDMs && (channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup) { return nil, errors.New("cannot share a direct or group channel") } // check if channel is already shared scExisting, err := scs.server.GetStore().SharedChannel().Get(sc.ChannelId) if err == nil { // already shared, nothing to do return scExisting, nil } if !isNotFoundError(err) { return nil, fmt.Errorf("cannot check if channel %s is shared: %w", sc.ChannelId, err) } if sc.TeamId == "" { sc.TeamId = channel.TeamId } if sc.Type == "" { sc.Type = channel.Type } if sc.ShareName == "" { sc.ShareName = channel.Name } if sc.ShareDisplayName == "" { sc.ShareDisplayName = channel.DisplayName } if sc.SharePurpose == "" { sc.SharePurpose = channel.Purpose } if sc.ShareHeader == "" { sc.ShareHeader = channel.Header } if sc.CreatorId == "" { sc.CreatorId = channel.CreatorId } // stores the SharedChannel and sets the share flag on the channel. scNew, err := scs.server.GetStore().SharedChannel().Save(sc) if err != nil { return nil, err } // to avoid fetching the channel again, we manually set the shared // flag before notifying the clients channel.Shared = model.NewPointer(true) scs.notifyClientsForSharedChannelConverted(channel) return scNew, nil } // UpdateSharedChannel updates the shared channel details such as displayname, purpose, or header func (scs *Service) UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error) { channel, err := scs.server.GetStore().Channel().Get(sc.ChannelId, true) if err != nil { return nil, err } scUpdated, err := scs.server.GetStore().SharedChannel().Update(sc) if err != nil { return nil, err } scs.notifyClientsForSharedChannelUpdate(channel) return scUpdated, nil } // UnshareChannel unshares the channel by deleting the SharedChannels record and unsets the Channel `shared` flag. // Returns true if a shared channel existed and was deleted. func (scs *Service) UnshareChannel(channelID string) (bool, error) { channel, err := scs.server.GetStore().Channel().Get(channelID, true) if err != nil { return false, err } // deletes the ShareChannel, unsets the share flag on the channel, deletes all records in SharedChannelRemotes for the channel. deleted, err := scs.server.GetStore().SharedChannel().Delete(channelID) if err != nil { return false, err } // to avoid fetching the channel again, we manually set the shared // flag before notifying the clients channel.Shared = model.NewPointer(false) scs.notifyClientsForSharedChannelConverted(channel) return deleted, nil } // InviteRemoteToChannel sends an invite to the remote to a shared channel. If `shareIfNotShared` is true // then the channel is marked as `shared` first if needed. func (scs *Service) InviteRemoteToChannel(channelID, remoteID, userID string, shareIfNotShared bool) error { scStore := scs.server.GetStore().SharedChannel() rcStore := scs.server.GetStore().RemoteCluster() // check if remote already invited to channel hasRemote, err := scStore.HasRemote(channelID, remoteID) if err != nil { return model.NewAppError("InviteRemoteToChannel", "api.command_share.fetch_remote.error", map[string]any{"Error": err.Error()}, "", http.StatusInternalServerError) } if hasRemote { // already invited, nothing to do return nil } // set the channel `shared` flag if needed if shareIfNotShared { sc := &model.SharedChannel{ ChannelId: channelID, CreatorId: userID, Home: true, RemoteId: "", // channel originates locally } if _, err = scs.ShareChannel(sc); err != nil { return model.NewAppError("InviteRemoteToChannel", "api.command_share.share_channel.error", map[string]any{"Error": err.Error()}, "", http.StatusBadRequest) } } else { if err = scs.CheckChannelIsShared(channelID); err != nil { return model.NewAppError("InviteRemoteToChannel", "api.command_share.channel_not_shared.error", map[string]any{"ChannelID": channelID}, "", http.StatusBadRequest) } } rc, err := rcStore.Get(remoteID, false) if err != nil { return model.NewAppError("InviteRemoteToChannel", "api.command_share.remote_id_invalid.error", map[string]any{"Error": err.Error()}, "", http.StatusInternalServerError).Wrap(err) } // don't allow invitation to shared channel originating from remote. // (also blocks cyclic invitations) if err = scs.CheckCanInviteToSharedChannel(channelID); err != nil { if errors.Is(err, model.ErrChannelHomedOnRemote) { return model.NewAppError("InviteRemoteToChannel", "api.command_share.channel_invite_not_home.error", nil, "", http.StatusBadRequest) } scs.server.Log().Debug("InviteRemoteToChannel failed to check if can-invite", mlog.String("name", rc.Name), mlog.String("channel_id", channelID), mlog.Err(err), ) return model.NewAppError("InviteRemoteToChannel", "api.command_share.channel_invite.error", map[string]any{"Name": rc.DisplayName, "Error": err.Error()}, "CheckCanInviteToSharedChannel", http.StatusInternalServerError).Wrap(err) } channel, err := scs.server.GetStore().Channel().Get(channelID, true) if err != nil { return model.NewAppError("InviteRemoteToChannel", "api.command_share.channel_invite.error", map[string]any{"Name": rc.DisplayName, "Error": err.Error()}, "", http.StatusInternalServerError).Wrap(err) } // send channel invite to remote cluster. Will notify clients of channel change. if err := scs.SendChannelInvite(channel, userID, rc); err != nil { return model.NewAppError("InviteRemoteToChannel", "api.command_share.channel_invite.error", map[string]any{"Name": rc.DisplayName, "Error": err.Error()}, "", http.StatusInternalServerError).Wrap(err) } return nil } // unshareChannelIfNoActiveRemotes checks if there are any remaining // non-deleted remotes for the channel and unshares the channel if // there are none. Returns true if the channel was unshared. func (scs *Service) unshareChannelIfNoActiveRemotes(channelID string) (bool, error) { opts := model.SharedChannelRemoteFilterOpts{ChannelId: channelID} remotes, err := scs.server.GetStore().SharedChannel().GetRemotes(0, 1, opts) if err != nil { return false, fmt.Errorf("failed to check remaining remotes: %w", err) } // If no remotes remain, unshare the channel if len(remotes) == 0 { unshared, err := scs.UnshareChannel(channelID) if err != nil { return false, fmt.Errorf("failed to automatically unshare channel after removing last remote: %w", err) } return unshared, nil } return false, nil } func (scs *Service) UninviteRemoteFromChannel(channelID, remoteID string) error { scr, err := scs.server.GetStore().SharedChannel().GetRemoteByIds(channelID, remoteID) if err != nil || scr.ChannelId != channelID || scr.DeleteAt != 0 { return model.NewAppError("UninviteRemoteFromChannel", "api.command_share.channel_remote_id_not_exists", map[string]any{"RemoteId": remoteID}, "", http.StatusInternalServerError) } deleted, err := scs.server.GetStore().SharedChannel().DeleteRemote(scr.Id) if err != nil || !deleted { code := http.StatusInternalServerError if err == nil { err = errors.New("not found") code = http.StatusBadRequest } return model.NewAppError("UninviteRemoteFromChannel", "api.command_share.could_not_uninvite.error", map[string]any{"RemoteId": remoteID, "Error": err.Error()}, "", code) } _, unshareErr := scs.unshareChannelIfNoActiveRemotes(channelID) if unshareErr != nil { // We don't want to fail the uninvite operation if the unshare fails scs.server.Log().Error("Error during automatic unshare after uninvite", mlog.String("channel_id", channelID), mlog.Err(unshareErr), ) } return nil } // CheckChannelNotShared returns nil only if the channel is not already shared. Otherwise ErrChannelAlreadyShared is // returned if the channel is shared, or database error. func (scs *Service) CheckChannelNotShared(channelID string) error { // check that channel exists. if _, err := scs.server.GetStore().Channel().Get(channelID, true); err != nil { return fmt.Errorf("cannot find channel %s: %w", channelID, err) } // Check channel is not already shared. if _, err := scs.server.GetStore().SharedChannel().Get(channelID); err == nil { return model.ErrChannelAlreadyShared } return nil } // CheckChannelIsShared returns nil only if the channel is shared. Otherwise a store.ErrNotFound is returned // or database error. func (scs *Service) CheckChannelIsShared(channelID string) error { if _, err := scs.server.GetStore().SharedChannel().Get(channelID); err != nil { var errNotFound *store.ErrNotFound if errors.As(err, &errNotFound) { return fmt.Errorf("channel is not shared: %w", errNotFound) } return fmt.Errorf("cannot check if channel %s is shared: %w", channelID, err) } return nil } // CheckCanInviteToSharedChannel checks if an invitation can be sent for the specified channel. // - don't allow invitations to a shared channel originating from remote. // - block cyclic invitations // - the channel must exist func (scs *Service) CheckCanInviteToSharedChannel(channelId string) error { sc, err := scs.server.GetStore().SharedChannel().Get(channelId) if err != nil { if isNotFoundError(err) { return fmt.Errorf("channel is not shared: %w", err) } return fmt.Errorf("cannot find channel: %w", err) } if !sc.Home { return model.ErrChannelHomedOnRemote } return nil } // updateMembershipSyncCursor updates the LastMembersSyncAt value for the shared channel remote // This provides centralized and consistent cursor management func (scs *Service) updateMembershipSyncCursor(channelID string, remoteID string, newTimestamp int64) error { // Get the remote record scr, err := scs.server.GetStore().SharedChannel().GetRemoteByIds(channelID, remoteID) if err != nil { scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to get shared channel remote for cursor update", mlog.String("channel_id", channelID), mlog.String("remote_id", remoteID), mlog.Int("timestamp", int(newTimestamp)), mlog.Err(err), ) return fmt.Errorf("failed to get shared channel remote for cursor update: %w", err) } if scr == nil { scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Shared channel remote not found for cursor update", mlog.String("channel_id", channelID), mlog.String("remote_id", remoteID), ) return fmt.Errorf("shared channel remote not found for channel %s and remote %s", channelID, remoteID) } // Update the cursor - the store will handle ensuring it only moves forward err = scs.server.GetStore().SharedChannel().UpdateRemoteMembershipCursor(scr.Id, newTimestamp) if err != nil { scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to update membership cursor", mlog.String("channel_id", channelID), mlog.String("remote_id", remoteID), mlog.String("remote_record_id", scr.Id), mlog.Int("timestamp", int(newTimestamp)), mlog.Err(err), ) } return err }