github.com/masterhung0112/hk_server/v5@v5.0.0-20220302090640-ec71aef15e1c/services/sharedchannel/service.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package sharedchannel
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"net/url"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/masterhung0112/hk_server/v5/app/request"
    14  	"github.com/masterhung0112/hk_server/v5/model"
    15  	"github.com/masterhung0112/hk_server/v5/services/remotecluster"
    16  	"github.com/masterhung0112/hk_server/v5/shared/filestore"
    17  	"github.com/masterhung0112/hk_server/v5/shared/mlog"
    18  	"github.com/masterhung0112/hk_server/v5/store"
    19  )
    20  
    21  const (
    22  	TopicSync                    = "sharedchannel_sync"
    23  	TopicChannelInvite           = "sharedchannel_invite"
    24  	TopicUploadCreate            = "sharedchannel_upload"
    25  	MaxRetries                   = 3
    26  	MaxPostsPerSync              = 12 // a bit more than one typical screenfull of posts
    27  	MaxUsersPerSync              = 25
    28  	NotifyRemoteOfflineThreshold = time.Second * 10
    29  	NotifyMinimumDelay           = time.Second * 2
    30  	MaxUpsertRetries             = 25
    31  	ProfileImageSyncTimeout      = time.Second * 5
    32  	KeyRemoteUsername            = "RemoteUsername"
    33  	KeyRemoteEmail               = "RemoteEmail"
    34  )
    35  
    36  // Mocks can be re-generated with `make sharedchannel-mocks`.
    37  type ServerIface interface {
    38  	Config() *model.Config
    39  	IsLeader() bool
    40  	AddClusterLeaderChangedListener(listener func()) string
    41  	RemoveClusterLeaderChangedListener(id string)
    42  	GetStore() store.Store
    43  	GetLogger() mlog.LoggerIFace
    44  	GetRemoteClusterService() remotecluster.RemoteClusterServiceIFace
    45  }
    46  
    47  type AppIface interface {
    48  	SendEphemeralPost(userId string, post *model.Post) *model.Post
    49  	CreateChannelWithUser(c *request.Context, channel *model.Channel, userId string) (*model.Channel, *model.AppError)
    50  	GetOrCreateDirectChannel(c *request.Context, userId, otherUserId string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError)
    51  	AddUserToChannel(user *model.User, channel *model.Channel, skipTeamMemberIntegrityCheck bool) (*model.ChannelMember, *model.AppError)
    52  	AddUserToTeamByTeamId(c *request.Context, teamId string, user *model.User) *model.AppError
    53  	PermanentDeleteChannel(channel *model.Channel) *model.AppError
    54  	CreatePost(c *request.Context, post *model.Post, channel *model.Channel, triggerWebhooks bool, setOnline bool) (savedPost *model.Post, err *model.AppError)
    55  	UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) (*model.Post, *model.AppError)
    56  	DeletePost(postID, deleteByID string) (*model.Post, *model.AppError)
    57  	SaveReactionForPost(c *request.Context, reaction *model.Reaction) (*model.Reaction, *model.AppError)
    58  	DeleteReactionForPost(c *request.Context, reaction *model.Reaction) *model.AppError
    59  	PatchChannelModerationsForChannel(channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError)
    60  	CreateUploadSession(us *model.UploadSession) (*model.UploadSession, *model.AppError)
    61  	FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError)
    62  	MentionsToTeamMembers(message, teamID string) model.UserMentionMap
    63  	GetProfileImage(user *model.User) ([]byte, bool, *model.AppError)
    64  	InvalidateCacheForUser(userID string)
    65  	NotifySharedChannelUserUpdate(user *model.User)
    66  }
    67  
    68  // errNotFound allows checking against Store.ErrNotFound errors without making Store a dependency.
    69  type errNotFound interface {
    70  	IsErrNotFound() bool
    71  }
    72  
    73  // errInvalidInput allows checking against Store.ErrInvalidInput errors without making Store a dependency.
    74  type errInvalidInput interface {
    75  	InvalidInputInfo() (entity string, field string, value interface{})
    76  }
    77  
    78  // Service provides shared channel synchronization.
    79  type Service struct {
    80  	server       ServerIface
    81  	app          AppIface
    82  	changeSignal chan struct{}
    83  
    84  	// everything below guarded by `mux`
    85  	mux                       sync.RWMutex
    86  	active                    bool
    87  	leaderListenerId          string
    88  	connectionStateListenerId string
    89  	done                      chan struct{}
    90  	tasks                     map[string]syncTask
    91  	syncTopicListenerId       string
    92  	inviteTopicListenerId     string
    93  	uploadTopicListenerId     string
    94  	siteURL                   *url.URL
    95  }
    96  
    97  // NewSharedChannelService creates a RemoteClusterService instance.
    98  func NewSharedChannelService(server ServerIface, app AppIface) (*Service, error) {
    99  	service := &Service{
   100  		server:       server,
   101  		app:          app,
   102  		changeSignal: make(chan struct{}, 1),
   103  		tasks:        make(map[string]syncTask),
   104  	}
   105  	parsed, err := url.Parse(*server.Config().ServiceSettings.SiteURL)
   106  	if err != nil {
   107  		return nil, fmt.Errorf("unable to parse SiteURL: %w", err)
   108  	}
   109  	service.siteURL = parsed
   110  	return service, nil
   111  }
   112  
   113  // Start is called by the server on server start-up.
   114  func (scs *Service) Start() error {
   115  	rcs := scs.server.GetRemoteClusterService()
   116  	if rcs == nil {
   117  		return errors.New("Shared Channel Service cannot activate: requires Remote Cluster Service")
   118  	}
   119  
   120  	scs.mux.Lock()
   121  	scs.leaderListenerId = scs.server.AddClusterLeaderChangedListener(scs.onClusterLeaderChange)
   122  	scs.syncTopicListenerId = rcs.AddTopicListener(TopicSync, scs.onReceiveSyncMessage)
   123  	scs.inviteTopicListenerId = rcs.AddTopicListener(TopicChannelInvite, scs.onReceiveChannelInvite)
   124  	scs.uploadTopicListenerId = rcs.AddTopicListener(TopicUploadCreate, scs.onReceiveUploadCreate)
   125  	scs.connectionStateListenerId = rcs.AddConnectionStateListener(scs.onConnectionStateChange)
   126  	scs.mux.Unlock()
   127  
   128  	scs.onClusterLeaderChange()
   129  
   130  	return nil
   131  }
   132  
   133  // Shutdown is called by the server on server shutdown.
   134  func (scs *Service) Shutdown() error {
   135  	rcs := scs.server.GetRemoteClusterService()
   136  	if rcs == nil {
   137  		return errors.New("Shared Channel Service cannot shutdown: requires Remote Cluster Service")
   138  	}
   139  
   140  	scs.mux.Lock()
   141  	id := scs.leaderListenerId
   142  	rcs.RemoveTopicListener(scs.syncTopicListenerId)
   143  	scs.syncTopicListenerId = ""
   144  	rcs.RemoveTopicListener(scs.inviteTopicListenerId)
   145  	scs.inviteTopicListenerId = ""
   146  	rcs.RemoveConnectionStateListener(scs.connectionStateListenerId)
   147  	scs.connectionStateListenerId = ""
   148  	scs.mux.Unlock()
   149  
   150  	scs.server.RemoveClusterLeaderChangedListener(id)
   151  	scs.pause()
   152  	return nil
   153  }
   154  
   155  // Active determines whether the service is active on the node or not.
   156  func (scs *Service) Active() bool {
   157  	scs.mux.Lock()
   158  	defer scs.mux.Unlock()
   159  
   160  	return scs.active
   161  }
   162  
   163  func (scs *Service) sendEphemeralPost(channelId string, userId string, text string) {
   164  	ephemeral := &model.Post{
   165  		ChannelId: channelId,
   166  		Message:   text,
   167  		CreateAt:  model.GetMillis(),
   168  	}
   169  	scs.app.SendEphemeralPost(userId, ephemeral)
   170  }
   171  
   172  // onClusterLeaderChange is called whenever the cluster leader may have changed.
   173  func (scs *Service) onClusterLeaderChange() {
   174  	if scs.server.IsLeader() {
   175  		scs.resume()
   176  	} else {
   177  		scs.pause()
   178  	}
   179  }
   180  
   181  func (scs *Service) resume() {
   182  	scs.mux.Lock()
   183  	defer scs.mux.Unlock()
   184  
   185  	if scs.active {
   186  		return // already active
   187  	}
   188  
   189  	scs.active = true
   190  	scs.done = make(chan struct{})
   191  
   192  	go scs.syncLoop(scs.done)
   193  
   194  	scs.server.GetLogger().Debug("Shared Channel Service active")
   195  }
   196  
   197  func (scs *Service) pause() {
   198  	scs.mux.Lock()
   199  	defer scs.mux.Unlock()
   200  
   201  	if !scs.active {
   202  		return // already inactive
   203  	}
   204  
   205  	scs.active = false
   206  	close(scs.done)
   207  	scs.done = nil
   208  
   209  	scs.server.GetLogger().Debug("Shared Channel Service inactive")
   210  }
   211  
   212  // Makes the remote channel to be read-only(announcement mode, only admins can create posts and reactions).
   213  func (scs *Service) makeChannelReadOnly(channel *model.Channel) *model.AppError {
   214  	createPostPermission := model.ChannelModeratedPermissionsMap[model.PERMISSION_CREATE_POST.Id]
   215  	createReactionPermission := model.ChannelModeratedPermissionsMap[model.PERMISSION_ADD_REACTION.Id]
   216  	updateMap := model.ChannelModeratedRolesPatch{
   217  		Guests:  model.NewBool(false),
   218  		Members: model.NewBool(false),
   219  	}
   220  
   221  	readonlyChannelModerations := []*model.ChannelModerationPatch{
   222  		{
   223  			Name:  &createPostPermission,
   224  			Roles: &updateMap,
   225  		},
   226  		{
   227  			Name:  &createReactionPermission,
   228  			Roles: &updateMap,
   229  		},
   230  	}
   231  
   232  	_, err := scs.app.PatchChannelModerationsForChannel(channel, readonlyChannelModerations)
   233  	return err
   234  }
   235  
   236  // onConnectionStateChange is called whenever the connection state of a remote cluster changes,
   237  // for example when one comes back online.
   238  func (scs *Service) onConnectionStateChange(rc *model.RemoteCluster, online bool) {
   239  	if online {
   240  		// when a previously offline remote comes back online force a sync.
   241  		scs.ForceSyncForRemote(rc)
   242  	}
   243  
   244  	scs.server.GetLogger().Log(mlog.LvlSharedChannelServiceDebug, "Remote cluster connection status changed",
   245  		mlog.String("remote", rc.DisplayName),
   246  		mlog.String("remoteId", rc.RemoteId),
   247  		mlog.Bool("online", online),
   248  	)
   249  }