Plugin API hook for Shared Channel file attachment sync (#25874)

* option for auto inviting plugin to all shared channels.

* auto-invite remotes to shared channels when flag set

* fix unit test

* immediately ping new remotes; fix unique siteurl bug

* make i18n-extract

* fix translations

* plugin hooks for file attachments

* hook for profile image sync

* fix profile image sync

* fix unit test

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Doug Lauder
2024-01-16 09:48:51 -05:00
коммит произвёл GitHub
родитель d90d3e4036
Коммит a07097ed57
21 изменённых файлов: 461 добавлений и 76 удалений

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

@@ -66,7 +66,8 @@ func (rcs *Service) pingGenerator(pingChan chan *model.RemoteCluster, done <-cha
}
for _, rc := range remotes {
if rc.SiteURL != "" || rc.PluginID != "" { // filter out unconfirmed invites
// filter out unconfirmed invites so we don't ping them without permission
if rc.IsConfirmed() {
pingChan <- rc
}
}

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

@@ -41,6 +41,10 @@ func (scs *Service) sendAttachmentForRemote(fi *model.FileInfo, post *model.Post
return fmt.Errorf("cannot update remote cluster for remote id %s; Remote Cluster Service not enabled", rc.RemoteId)
}
if rc.IsPlugin() {
return scs.sendAttachmentToPlugin(fi, post, rc)
}
us := &model.UploadSession{
Id: model.NewId(),
Type: model.UploadTypeAttachment,
@@ -120,11 +124,7 @@ func (scs *Service) sendAttachmentForRemote(fi *model.FileInfo, post *model.Post
}
// save file attachment record in SharedChannelAttachments table
sca := &model.SharedChannelAttachment{
FileId: fi.Id,
RemoteId: rc.RemoteId,
}
if _, err2 := scs.server.GetStore().SharedChannel().UpsertAttachment(sca); err2 != nil {
if err2 := scs.saveSharedAttachment(&fi, rc); err2 != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "error saving SharedChannelAttachment",
mlog.String("remote", rc.DisplayName),
mlog.String("uploadId", usResp.Id),
@@ -140,6 +140,24 @@ func (scs *Service) sendAttachmentForRemote(fi *model.FileInfo, post *model.Post
})
}
// sendAttachmentToPlugin asynchronously sends a file attachment to a remote cluster.
func (scs *Service) sendAttachmentToPlugin(fi *model.FileInfo, post *model.Post, rc *model.RemoteCluster) error {
if err := scs.app.OnSharedChannelsAttachmentSyncMsg(fi, post, rc); err != nil {
return fmt.Errorf("cannot send attachment to plugin %s: %w", rc.PluginID, err)
}
return scs.saveSharedAttachment(fi, rc)
}
// saveSharedAttachment saves the attachment in SharedChannelAttachments table.
func (scs *Service) saveSharedAttachment(fi *model.FileInfo, rc *model.RemoteCluster) error {
sca := &model.SharedChannelAttachment{
FileId: fi.Id,
RemoteId: rc.RemoteId,
}
_, err := scs.server.GetStore().SharedChannel().UpsertAttachment(sca)
return err
}
// onReceiveUploadCreate is called when a message requesting to create an upload session is received. An upload session is
// created and the id returned in the response.
func (scs *Service) onReceiveUploadCreate(msg model.RemoteClusterMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {

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

@@ -314,6 +314,34 @@ func (_m *MockAppIface) NotifySharedChannelUserUpdate(user *model.User) {
_m.Called(user)
}
// OnSharedChannelsAttachmentSyncMsg provides a mock function with given fields: fi, post, rc
func (_m *MockAppIface) OnSharedChannelsAttachmentSyncMsg(fi *model.FileInfo, post *model.Post, rc *model.RemoteCluster) error {
ret := _m.Called(fi, post, rc)
var r0 error
if rf, ok := ret.Get(0).(func(*model.FileInfo, *model.Post, *model.RemoteCluster) error); ok {
r0 = rf(fi, post, rc)
} else {
r0 = ret.Error(0)
}
return r0
}
// OnSharedChannelsProfileImageSyncMsg provides a mock function with given fields: user, rc
func (_m *MockAppIface) OnSharedChannelsProfileImageSyncMsg(user *model.User, rc *model.RemoteCluster) error {
ret := _m.Called(user, rc)
var r0 error
if rf, ok := ret.Get(0).(func(*model.User, *model.RemoteCluster) error); ok {
r0 = rf(user, rc)
} else {
r0 = ret.Error(0)
}
return r0
}
// OnSharedChannelsSyncMsg provides a mock function with given fields: msg, rc
func (_m *MockAppIface) OnSharedChannelsSyncMsg(msg *model.SyncMsg, rc *model.RemoteCluster) (model.SyncResponse, error) {
ret := _m.Called(msg, rc)

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

@@ -64,6 +64,8 @@ type AppIface interface {
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
}
// errNotFound allows checking against Store.ErrNotFound errors without making Store a dependency.

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

@@ -402,11 +402,17 @@ func (scs *Service) shouldUserSync(user *model.User, channelID string, rc *model
}
if _, err = scs.server.GetStore().SharedChannel().SaveUser(scu); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error adding user to shared channel users",
mlog.String("remote_id", rc.RemoteId),
mlog.String("user_id", user.Id),
mlog.String("channel_id", user.Id),
mlog.String("remote_id", rc.RemoteId),
mlog.Err(err),
)
} else {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Added user to shared channel users",
mlog.String("user_id", user.Id),
mlog.String("channel_id", user.Id),
mlog.String("remote_id", rc.RemoteId),
)
}
return true, true, nil
}
@@ -420,30 +426,55 @@ func (scs *Service) syncProfileImage(user *model.User, channelID string, rc *mod
return
}
if rc.IsPlugin() {
scs.sendProfileImageToPlugin(user, channelID, rc)
return
}
ctx, cancel := context.WithTimeout(context.Background(), ProfileImageSyncTimeout)
defer cancel()
rcs.SendProfileImage(ctx, user.Id, rc, scs.app, func(userId string, rc *model.RemoteCluster, resp *remotecluster.Response, err error) {
if resp.IsSuccess() {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Users profile image synchronized",
mlog.String("remote_id", rc.RemoteId),
mlog.String("user_id", user.Id),
)
if err2 := scs.server.GetStore().SharedChannel().UpdateUserLastSyncAt(user.Id, channelID, rc.RemoteId); err2 != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error updating users LastSyncTime after profile image update",
mlog.String("remote_id", rc.RemoteId),
mlog.String("user_id", user.Id),
mlog.Err(err2),
)
}
scs.recordProfileImageSuccess(user.Id, channelID, rc.RemoteId)
return
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error synchronizing users profile image",
mlog.String("remote_id", rc.RemoteId),
mlog.String("user_id", user.Id),
mlog.String("channel_id", channelID),
mlog.String("remote_id", rc.RemoteId),
mlog.Err(err),
)
})
}
func (scs *Service) sendProfileImageToPlugin(user *model.User, channelID string, rc *model.RemoteCluster) {
if err := scs.app.OnSharedChannelsProfileImageSyncMsg(user, rc); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error synchronizing users profile image for plugin",
mlog.String("user_id", user.Id),
mlog.String("channel_id", channelID),
mlog.String("remote_id", rc.RemoteId),
mlog.Err(err),
)
}
scs.recordProfileImageSuccess(user.Id, channelID, rc.RemoteId)
}
func (scs *Service) recordProfileImageSuccess(userID, channelID, remoteID string) {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Users profile image synchronized",
mlog.String("user_id", userID),
mlog.String("channel_id", channelID),
mlog.String("remote_id", remoteID),
)
// update LastSyncAt for user in SharedChannelUsers table
if err := scs.server.GetStore().SharedChannel().UpdateUserLastSyncAt(userID, channelID, remoteID); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error updating users LastSyncTime after profile image update",
mlog.String("user_id", userID),
mlog.String("channel_id", channelID),
mlog.String("remote_id", remoteID),
mlog.Err(err),
)
}
}

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

@@ -235,13 +235,17 @@ func (scs *Service) fetchPostsForSync(sd *syncData) error {
count := len(posts)
sd.posts = appendPosts(sd.posts, posts, scs.server.GetStore().Post(), cursor.LastPostCreateAt)
cache := postsSliceToMap(posts)
// Fill remaining batch capacity with updated posts.
if len(posts) < MaxPostsPerSync {
options.SinceCreateAt = false
// use 'nextcursor' as it has the correct xxxUpdateAt values, and the updsted xxxCreateAt values.
posts, nextCursor, err = scs.server.GetStore().Post().GetPostsSinceForSync(options, nextCursor, MaxPostsPerSync-len(posts))
if err != nil {
return fmt.Errorf("could not fetch modified posts for sync: %w", err)
}
posts = reducePostsSliceInCache(posts, cache)
count += len(posts)
sd.posts = appendPosts(sd.posts, posts, scs.server.GetStore().Post(), cursor.LastPostUpdateAt)
}

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

@@ -130,3 +130,21 @@ func isNotFoundError(err error) bool {
var errNotFound *store.ErrNotFound
return errors.As(err, &errNotFound)
}
func postsSliceToMap(posts []*model.Post) map[string]*model.Post {
m := make(map[string]*model.Post, len(posts))
for _, p := range posts {
m[p.Id] = p
}
return m
}
func reducePostsSliceInCache(posts []*model.Post, cache map[string]*model.Post) []*model.Post {
reduced := make([]*model.Post, 0, len(posts))
for _, p := range posts {
if _, ok := cache[p.Id]; !ok {
reduced = append(reduced, p)
}
}
return reduced
}