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 }