Shared channels auto-share DM & group messages (#26097)
* option for auto inviting plugin to all shared channels. * auto-invite remotes to shared channels when flag set
Этот коммит содержится в:
@@ -288,11 +288,6 @@ func (_m *MockAppIface) GetProfileImage(user *model.User) ([]byte, bool, *model.
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// InvalidateCacheForUser provides a mock function with given fields: userID
|
||||
func (_m *MockAppIface) InvalidateCacheForUser(userID string) {
|
||||
_m.Called(userID)
|
||||
}
|
||||
|
||||
// MentionsToTeamMembers provides a mock function with given fields: c, message, teamID
|
||||
func (_m *MockAppIface) MentionsToTeamMembers(c request.CTX, message string, teamID string) model.UserMentionMap {
|
||||
ret := _m.Called(c, message, teamID)
|
||||
@@ -410,6 +405,11 @@ func (_m *MockAppIface) PermanentDeleteChannel(c request.CTX, channel *model.Cha
|
||||
return r0
|
||||
}
|
||||
|
||||
// Publish provides a mock function with given fields: message
|
||||
func (_m *MockAppIface) Publish(message *model.WebSocketEvent) {
|
||||
_m.Called(message)
|
||||
}
|
||||
|
||||
// SaveReactionForPost provides a mock function with given fields: c, reaction
|
||||
func (_m *MockAppIface) SaveReactionForPost(c request.CTX, reaction *model.Reaction) (*model.Reaction, *model.AppError) {
|
||||
ret := _m.Called(c, reaction)
|
||||
|
||||
@@ -44,6 +44,11 @@ type ServerIface interface {
|
||||
GetRemoteClusterService() remotecluster.RemoteClusterServiceIFace
|
||||
}
|
||||
|
||||
type PlatformIface interface {
|
||||
InvalidateCacheForUser(userID string)
|
||||
InvalidateCacheForChannel(channel *model.Channel)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -61,11 +66,11 @@ type AppIface interface {
|
||||
FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError)
|
||||
MentionsToTeamMembers(c request.CTX, message, teamID string) model.UserMentionMap
|
||||
GetProfileImage(user *model.User) ([]byte, bool, *model.AppError)
|
||||
InvalidateCacheForUser(userID string)
|
||||
NotifySharedChannelUserUpdate(user *model.User)
|
||||
OnSharedChannelsSyncMsg(msg *model.SyncMsg, rc *model.RemoteCluster) (model.SyncResponse, error)
|
||||
OnSharedChannelsAttachmentSyncMsg(fi *model.FileInfo, post *model.Post, rc *model.RemoteCluster) error
|
||||
OnSharedChannelsProfileImageSyncMsg(user *model.User, rc *model.RemoteCluster) error
|
||||
Publish(message *model.WebSocketEvent)
|
||||
}
|
||||
|
||||
// errNotFound allows checking against Store.ErrNotFound errors without making Store a dependency.
|
||||
@@ -76,6 +81,7 @@ type errNotFound interface {
|
||||
// Service provides shared channel synchronization.
|
||||
type Service struct {
|
||||
server ServerIface
|
||||
platform PlatformIface
|
||||
app AppIface
|
||||
changeSignal chan struct{}
|
||||
|
||||
@@ -93,9 +99,10 @@ type Service struct {
|
||||
}
|
||||
|
||||
// NewSharedChannelService creates a RemoteClusterService instance.
|
||||
func NewSharedChannelService(server ServerIface, app AppIface) (*Service, error) {
|
||||
func NewSharedChannelService(server ServerIface, platform PlatformIface, app AppIface) (*Service, error) {
|
||||
service := &Service{
|
||||
server: server,
|
||||
platform: platform,
|
||||
app: app,
|
||||
changeSignal: make(chan struct{}, 1),
|
||||
tasks: make(map[string]syncTask),
|
||||
@@ -245,3 +252,17 @@ func (scs *Service) onConnectionStateChange(rc *model.RemoteCluster, online bool
|
||||
mlog.Bool("online", online),
|
||||
)
|
||||
}
|
||||
|
||||
func (scs *Service) notifyClientsForSharedChannelConverted(channel *model.Channel) {
|
||||
scs.platform.InvalidateCacheForChannel(channel)
|
||||
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelConverted, channel.TeamId, "", "", nil, "")
|
||||
messageWs.Add("channel_id", channel.Id)
|
||||
scs.app.Publish(messageWs)
|
||||
}
|
||||
|
||||
func (scs *Service) notifyClientsForSharedChannelUpdate(channel *model.Channel) {
|
||||
scs.platform.InvalidateCacheForChannel(channel)
|
||||
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelUpdated, channel.TeamId, "", "", nil, "")
|
||||
messageWs.Add("channel_id", channel.Id)
|
||||
scs.app.Publish(messageWs)
|
||||
}
|
||||
|
||||
231
server/platform/services/sharedchannel/service_api.go
Обычный файл
231
server/platform/services/sharedchannel/service_api.go
Обычный файл
@@ -0,0 +1,231 @@
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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 unshared 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
|
||||
}
|
||||
|
||||
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 {
|
||||
if _, err = scs.ShareChannel(&model.SharedChannel{ChannelId: channelID, CreatorId: userID}); 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)
|
||||
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.StatusInternalServerError)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (scs *Service) UninviteRemoteFromChannel(channelID, remoteID string) error {
|
||||
scr, err := scs.server.GetStore().SharedChannel().GetRemoteByIds(channelID, remoteID)
|
||||
if err != nil || scr.ChannelId != channelID {
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -317,7 +317,7 @@ func (scs *Service) updateSyncUser(rctx request.CTX, patch *model.UserPatch, use
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scs.app.InvalidateCacheForUser(update.New.Id)
|
||||
scs.platform.InvalidateCacheForUser(update.New.Id)
|
||||
scs.app.NotifySharedChannelUserUpdate(update.New)
|
||||
return update.New, nil
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user