Shared channels plugin APIs for MS Teams plugin (#25805)

New plugin APIs and hooks for accessing Shared Channels service via plugin. 

- RegisterPluginForSharedChannels(opts model.RegisterPluginOpts) (remoteID string, err error)
- UnregisterPluginForSharedChannels(pluginID string) error
- ShareChannel(sc *model.SharedChannel) (*model.SharedChannel, error)
- UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error)
- UnshareChannel(channelID string) (unshared bool, err error)
- UpdateSharedChannelCursor(channelID, remoteID string, cusror model.GetPostsSinceForSyncCursor) error
- SyncSharedChannel(channelID string) error
- InviteRemoteToChannel(channelID string, remoteID string, userID string) error
- UninviteRemoteFromChannel(channelID string, remoteID string) error

Hooks
- OnSharedChannelsSyncMsg(msg *model.SyncMsg, rc *model.RemoteCluster) (model.SyncResponse, error)
- OnSharedChannelsPing(rc *model.RemoteCluster) bool
Этот коммит содержится в:
Doug Lauder
2023-12-22 17:00:27 -05:00
коммит произвёл GitHub
родитель 0f3553c8ab
Коммит 2d1135ca46
40 изменённых файлов: 1725 добавлений и 168 удалений

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

@@ -45,7 +45,7 @@ func TestGetAllSharedChannels(t *testing.T) {
CreatorId: th.BasicChannel.CreatorId,
RemoteId: model.NewId(),
}
_, err := th.App.SaveSharedChannel(th.Context, sc)
_, err := th.App.ShareChannel(th.Context, sc)
require.NoError(t, err)
savedIds = append(savedIds, channel.Id)
}
@@ -125,7 +125,7 @@ func TestGetRemoteClusterById(t *testing.T) {
CreatorId: th.BasicChannel.CreatorId,
RemoteId: rc.RemoteId,
}
sc, err := th.App.SaveSharedChannel(th.Context, sc)
sc, err := th.App.ShareChannel(th.Context, sc)
require.NoError(t, err)
// create a shared channel remote to connect them

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

@@ -269,6 +269,12 @@ type AppIface interface {
MoveChannel(c request.CTX, team *model.Team, channel *model.Channel, user *model.User) *model.AppError
// NotifySessionsExpired is called periodically from the job server to notify any mobile sessions that have expired.
NotifySessionsExpired() error
// OnSharedChannelsPing is called by the Shared Channels service for a registered plugin wto check that the plugin
// is still responding and has a connection to any upstream services it needs (e.g. MS Graph API).
OnSharedChannelsPing(rc *model.RemoteCluster) bool
// OnSharedChannelsSyncMsg is called by the Shared Channels service for a registered plugin when there is new content
// that needs to be synchronized.
OnSharedChannelsSyncMsg(msg *model.SyncMsg, rc *model.RemoteCluster) (model.SyncResponse, error)
// OverrideIconURLIfEmoji changes the post icon override URL prop, if it has an emoji icon,
// so that it points to the URL (relative) of the emoji - static if emoji is default, /api if custom.
OverrideIconURLIfEmoji(c request.CTX, post *model.Post)
@@ -339,6 +345,8 @@ type AppIface interface {
// SyncRolesAndMembership updates the SchemeAdmin status and membership of all of the members of the given
// syncable.
SyncRolesAndMembership(c request.CTX, syncableID string, syncableType model.GroupSyncableType, includeRemovedMembers bool)
// SyncSharedChannel forces a shared channel to send any changed content to all remote clusters.
SyncSharedChannel(channelID string) error
// SyncSyncableRoles updates the SchemeAdmin field value of the given syncable's members based on the configuration of
// the member's group memberships and the configuration of those groups to the syncable. This method should only
// be invoked on group-synced (aka group-constrained) syncables.
@@ -376,6 +384,12 @@ type AppIface interface {
UpdateDNDStatusOfUsers()
// UpdateProductNotices is called periodically from a scheduled worker to fetch new notices and update the cache
UpdateProductNotices() *model.AppError
// UpdateSharedChannelCursor updates the cursor for the specified channelID and remoteID.
// This can be used to manually set the point of last sync, either forward to skip older posts,
// or backward to re-sync history.
// This call by itself does not force a re-sync - a change to channel contents or a call to
// SyncSharedChannel are needed to force a sync.
UpdateSharedChannelCursor(channelID, remoteID string, cursor model.GetPostsSinceForSyncCursor) error
// UpdateViewedProductNotices is called from the frontend to mark a set of notices as 'viewed' by user
UpdateViewedProductNotices(userID string, noticeIds []string) *model.AppError
// UpdateViewedProductNoticesForNewUser is called when new user is created to mark all current notices for this
@@ -546,7 +560,6 @@ type AppIface interface {
DeleteRemoteCluster(remoteClusterId string) (bool, *model.AppError)
DeleteRetentionPolicy(policyID string) *model.AppError
DeleteScheme(schemeId string) (*model.Scheme, *model.AppError)
DeleteSharedChannel(channelID string) (bool, error)
DeleteSharedChannelRemote(id string) (bool, error)
DeleteSidebarCategory(c request.CTX, userID, teamID, categoryId string) *model.AppError
DeleteToken(token *model.Token) *model.AppError
@@ -888,6 +901,7 @@ type AppIface interface {
InviteGuestsToChannelsGracefully(teamID string, guestsInvite *model.GuestsInvite, senderId string) ([]*model.EmailInviteWithError, *model.AppError)
InviteNewUsersToTeam(emailList []string, teamID, senderId string) *model.AppError
InviteNewUsersToTeamGracefully(memberInvite *model.MemberInvite, teamID, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError)
InviteRemoteToChannel(channelID, remoteID, userID string) error
IsCRTEnabledForUser(c request.CTX, userID string) bool
IsConfigReadOnly() bool
IsFirstUserAccount() bool
@@ -968,6 +982,7 @@ type AppIface interface {
RegenerateOAuthAppSecret(app *model.OAuthApp) (*model.OAuthApp, *model.AppError)
RegenerateTeamInviteId(teamID string) (*model.Team, *model.AppError)
RegisterPluginCommand(pluginID string, command *model.Command) error
RegisterPluginForSharedChannels(opts model.RegisterPluginOpts) (remoteID string, err error)
ReloadConfig() error
RemoveAllDeactivatedMembersFromChannel(c request.CTX, channel *model.Channel) *model.AppError
RemoveChannelsFromRetentionPolicy(policyID string, channelIDs []string) *model.AppError
@@ -1016,7 +1031,6 @@ type AppIface interface {
SaveBrandImage(rctx request.CTX, imageData *multipart.FileHeader) *model.AppError
SaveComplianceReport(rctx request.CTX, job *model.Compliance) (*model.Compliance, *model.AppError)
SaveReactionForPost(c request.CTX, reaction *model.Reaction) (*model.Reaction, *model.AppError)
SaveSharedChannel(c request.CTX, sc *model.SharedChannel) (*model.SharedChannel, error)
SaveSharedChannelRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error)
SaveUserTermsOfService(userID, termsOfServiceId string, accepted bool) *model.AppError
SchemesIterator(scope string, batchSize int) func() []*model.Scheme
@@ -1092,6 +1106,7 @@ type AppIface interface {
SetTeamIcon(teamID string, imageData *multipart.FileHeader) *model.AppError
SetTeamIconFromFile(team *model.Team, file io.Reader) *model.AppError
SetTeamIconFromMultiPartFile(teamID string, file multipart.File) *model.AppError
ShareChannel(c request.CTX, sc *model.SharedChannel) (*model.SharedChannel, error)
SlackImport(c request.CTX, fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer)
SoftDeleteTeam(teamID string) *model.AppError
Srv() *Server
@@ -1112,7 +1127,10 @@ type AppIface interface {
ToggleMuteChannel(c request.CTX, channelID, userID string) (*model.ChannelMember, *model.AppError)
TotalWebsocketConnections() int
TriggerWebhook(c request.CTX, payload *model.OutgoingWebhookPayload, hook *model.OutgoingWebhook, post *model.Post, channel *model.Channel)
UninviteRemoteFromChannel(channelID, remoteID string) error
UnregisterPluginCommand(pluginID, teamID, trigger string)
UnregisterPluginForSharedChannels(pluginID string) error
UnshareChannel(channelID string) (bool, error)
UpdateActive(c request.CTX, user *model.User, active bool) (*model.User, *model.AppError)
UpdateChannelMemberNotifyProps(c request.CTX, data map[string]string, channelID string, userID string) (*model.ChannelMember, *model.AppError)
UpdateChannelMemberRoles(c request.CTX, channelID string, userID string, newRoles string) (*model.ChannelMember, *model.AppError)

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

@@ -560,7 +560,7 @@ func (a *App) createDirectChannelWithUser(c request.CTX, user, otherUser *model.
Type: channel.Type,
}
if _, err := a.SaveSharedChannel(c, sc); err != nil {
if _, err := a.ShareChannel(c, sc); err != nil {
return nil, model.NewAppError("CreateDirectChannel", "app.sharedchannel.dm_channel_creation.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}

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

@@ -374,7 +374,7 @@ func (th *TestHelper) createChannel(c request.CTX, team *model.Team, channelType
if channel.IsShared() {
id := model.NewId()
_, err := th.App.SaveSharedChannel(c, &model.SharedChannel{
_, err := th.App.ShareChannel(c, &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
Home: false,

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

@@ -3526,28 +3526,6 @@ func (a *OpenTracingAppLayer) DeleteScheme(schemeId string) (*model.Scheme, *mod
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteSharedChannel(channelID string) (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteSharedChannel(channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteSharedChannelRemote(id string) (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteSharedChannelRemote")
@@ -11883,6 +11861,28 @@ func (a *OpenTracingAppLayer) InviteNewUsersToTeamGracefully(memberInvite *model
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) InviteRemoteToChannel(channelID string, remoteID string, userID string) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InviteRemoteToChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.InviteRemoteToChannel(channelID, remoteID, userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) IsCRTEnabledForUser(c request.CTX, userID string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsCRTEnabledForUser")
@@ -12799,6 +12799,45 @@ func (a *OpenTracingAppLayer) NotifySharedChannelUserUpdate(user *model.User) {
a.app.NotifySharedChannelUserUpdate(user)
}
func (a *OpenTracingAppLayer) OnSharedChannelsPing(rc *model.RemoteCluster) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.OnSharedChannelsPing")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.OnSharedChannelsPing(rc)
return resultVar0
}
func (a *OpenTracingAppLayer) OnSharedChannelsSyncMsg(msg *model.SyncMsg, rc *model.RemoteCluster) (model.SyncResponse, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.OnSharedChannelsSyncMsg")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.OnSharedChannelsSyncMsg(msg, rc)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.OpenInteractiveDialog")
@@ -13718,6 +13757,28 @@ func (a *OpenTracingAppLayer) RegisterPluginCommand(pluginID string, command *mo
return resultVar0
}
func (a *OpenTracingAppLayer) RegisterPluginForSharedChannels(opts model.RegisterPluginOpts) (remoteID string, err error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegisterPluginForSharedChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RegisterPluginForSharedChannels(opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ReloadConfig() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ReloadConfig")
@@ -14848,28 +14909,6 @@ func (a *OpenTracingAppLayer) SaveReactionForPost(c request.CTX, reaction *model
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveSharedChannel(c request.CTX, sc *model.SharedChannel) (*model.SharedChannel, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveSharedChannel(c, sc)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveSharedChannelRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveSharedChannelRemote")
@@ -16566,6 +16605,28 @@ func (a *OpenTracingAppLayer) SetTeamIconFromMultiPartFile(teamID string, file m
return resultVar0
}
func (a *OpenTracingAppLayer) ShareChannel(c request.CTX, sc *model.SharedChannel) (*model.SharedChannel, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ShareChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ShareChannel(c, sc)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SlackImport(c request.CTX, fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SlackImport")
@@ -16772,6 +16833,28 @@ func (a *OpenTracingAppLayer) SyncRolesAndMembership(c request.CTX, syncableID s
a.app.SyncRolesAndMembership(c, syncableID, syncableType, includeRemovedMembers)
}
func (a *OpenTracingAppLayer) SyncSharedChannel(channelID string) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SyncSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SyncSharedChannel(channelID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SyncSyncableRoles(syncableID string, syncableType model.GroupSyncableType) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SyncSyncableRoles")
@@ -17063,6 +17146,28 @@ func (a *OpenTracingAppLayer) TriggerWebhook(c request.CTX, payload *model.Outgo
a.app.TriggerWebhook(c, payload, hook, post, channel)
}
func (a *OpenTracingAppLayer) UninviteRemoteFromChannel(channelID string, remoteID string) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UninviteRemoteFromChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UninviteRemoteFromChannel(channelID, remoteID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UnregisterPluginCommand(pluginID string, teamID string, trigger string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UnregisterPluginCommand")
@@ -17078,6 +17183,50 @@ func (a *OpenTracingAppLayer) UnregisterPluginCommand(pluginID string, teamID st
a.app.UnregisterPluginCommand(pluginID, teamID, trigger)
}
func (a *OpenTracingAppLayer) UnregisterPluginForSharedChannels(pluginID string) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UnregisterPluginForSharedChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UnregisterPluginForSharedChannels(pluginID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UnshareChannel(channelID string) (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UnshareChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UnshareChannel(channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateActive(c request.CTX, user *model.User, active bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateActive")
@@ -17866,6 +18015,28 @@ func (a *OpenTracingAppLayer) UpdateSharedChannel(sc *model.SharedChannel) (*mod
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateSharedChannelCursor(channelID string, remoteID string, cursor model.GetPostsSinceForSyncCursor) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSharedChannelCursor")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateSharedChannelCursor(channelID, remoteID, cursor)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateSharedChannelRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSharedChannelRemoteCursor")

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

@@ -1279,3 +1279,39 @@ func (api *PluginAPI) SendPushNotification(notification *model.PushNotification,
// Ignoring skipSessionId because it's only used internally to clear push notifications
return api.app.sendPushNotificationToAllSessions(notification, userID, "")
}
func (api *PluginAPI) RegisterPluginForSharedChannels(opts model.RegisterPluginOpts) (remoteID string, err error) {
return api.app.RegisterPluginForSharedChannels(opts)
}
func (api *PluginAPI) UnregisterPluginForSharedChannels(pluginID string) error {
return api.app.UnregisterPluginForSharedChannels(pluginID)
}
func (api *PluginAPI) ShareChannel(sc *model.SharedChannel) (*model.SharedChannel, error) {
return api.app.ShareChannel(api.ctx, sc)
}
func (api *PluginAPI) UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error) {
return api.app.UpdateSharedChannel(sc)
}
func (api *PluginAPI) UnshareChannel(channelID string) (unshared bool, err error) {
return api.app.UnshareChannel(channelID)
}
func (api *PluginAPI) UpdateSharedChannelCursor(channelID, remoteID string, cusror model.GetPostsSinceForSyncCursor) error {
return api.app.UpdateSharedChannelCursor(channelID, remoteID, cusror)
}
func (api *PluginAPI) SyncSharedChannel(channelID string) error {
return api.app.SyncSharedChannel(channelID)
}
func (api *PluginAPI) InviteRemoteToChannel(channelID string, remoteID, userID string) error {
return api.app.InviteRemoteToChannel(channelID, remoteID, userID)
}
func (api *PluginAPI) UninviteRemoteFromChannel(channelID string, remoteID string) error {
return api.app.UninviteRemoteFromChannel(channelID, remoteID)
}

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

@@ -4,6 +4,7 @@
package app
import (
"database/sql"
"net/http"
"github.com/pkg/errors"
@@ -14,6 +15,57 @@ import (
"github.com/mattermost/mattermost/server/public/model"
)
func (a *App) RegisterPluginForSharedChannels(opts model.RegisterPluginOpts) (remoteID string, err error) {
// check for pluginID already registered
rc, err := a.Srv().Store().RemoteCluster().GetByPluginID(opts.PluginID)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
// anything other than not_found is unrecoverable
return "", err
}
}
// if plugin is already registered then treat this as an update.
if rc != nil {
rc.DisplayName = opts.Displayname
rc.Options = opts.GetOptionFlags()
if _, err = a.Srv().Store().RemoteCluster().Update(rc); err != nil {
return "", err
}
return rc.RemoteId, nil
}
rc = &model.RemoteCluster{
Name: opts.Displayname,
DisplayName: opts.Displayname,
Token: model.NewId(),
CreatorId: opts.CreatorID,
PluginID: opts.PluginID,
Options: opts.GetOptionFlags(),
}
rcSaved, err := a.Srv().Store().RemoteCluster().Save(rc)
if err != nil {
return "", err
}
return rcSaved.RemoteId, nil
}
func (a *App) UnregisterPluginForSharedChannels(pluginID string) error {
rc, err := a.Srv().Store().RemoteCluster().GetByPluginID(pluginID)
if err != nil {
return err
}
_, appErr := a.DeleteRemoteCluster(rc.RemoteId)
if appErr != nil {
return appErr
}
return nil
}
func (a *App) AddRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) {
rc, err := a.Srv().Store().RemoteCluster().Save(rc)
if err != nil {

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

@@ -625,8 +625,8 @@ func (s *Server) startInterClusterServices(license *model.License) error {
}
var err error
rcs, err := remotecluster.NewRemoteClusterService(s)
appInstance := New(ServerConnector(s.Channels()))
rcs, err := remotecluster.NewRemoteClusterService(s, appInstance)
if err != nil {
return err
}
@@ -653,7 +653,6 @@ func (s *Server) startInterClusterServices(license *model.License) error {
return nil
}
appInstance := New(ServerConnector(s.Channels()))
scs, err := sharedchannel.NewSharedChannelService(s, appInstance)
if err != nil {
return err

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

@@ -13,6 +13,10 @@ import (
"github.com/mattermost/mattermost/server/v8/channels/store"
)
var (
errNotFound = errors.New("not found")
)
func (a *App) checkChannelNotShared(c request.CTX, channelId string) error {
// check that channel exists.
if _, err := a.GetChannel(c, channelId); err != nil {
@@ -57,13 +61,27 @@ func (a *App) CheckCanInviteToSharedChannel(channelId string) error {
return nil
}
func (a *App) notifyClientsForSharedChannelUpdate(teamID, channelID string) {
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelConverted, teamID, "", "", nil, "")
messageWs.Add("channel_id", channelID)
a.Publish(messageWs)
}
// SharedChannels
func (a *App) SaveSharedChannel(c request.CTX, sc *model.SharedChannel) (*model.SharedChannel, error) {
func (a *App) ShareChannel(c request.CTX, sc *model.SharedChannel) (*model.SharedChannel, error) {
if err := a.checkChannelNotShared(c, sc.ChannelId); err != nil {
return nil, err
}
return a.Srv().Store().SharedChannel().Save(sc)
// stores a SharedChannel and set the share flag on the channel.
scNew, err := a.Srv().Store().SharedChannel().Save(sc)
if err != nil {
return nil, err
}
a.notifyClientsForSharedChannelUpdate(scNew.TeamId, scNew.ChannelId)
return scNew, nil
}
func (a *App) GetSharedChannel(channelID string) (*model.SharedChannel, error) {
@@ -87,15 +105,105 @@ func (a *App) GetSharedChannelsCount(opts model.SharedChannelFilterOpts) (int64,
}
func (a *App) UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error) {
return a.Srv().Store().SharedChannel().Update(sc)
scUpdated, err := a.Srv().Store().SharedChannel().Update(sc)
if err != nil {
return nil, err
}
a.notifyClientsForSharedChannelUpdate(scUpdated.TeamId, scUpdated.ChannelId)
return scUpdated, nil
}
func (a *App) DeleteSharedChannel(channelID string) (bool, error) {
return a.Srv().Store().SharedChannel().Delete(channelID)
func (a *App) UnshareChannel(channelID string) (bool, error) {
// fetch the SharedChannel first
sc, err := a.GetSharedChannel(channelID)
if err != nil {
return false, err
}
// deletes the ShareChannel, unsets the share flag on the channel, deletes all remotes for the channel
deleted, err := a.Srv().Store().SharedChannel().Delete(channelID)
if err != nil {
return false, err
}
a.notifyClientsForSharedChannelUpdate(sc.TeamId, sc.ChannelId)
return deleted, nil
}
// SharedChannelRemotes
func (a *App) InviteRemoteToChannel(channelID, remoteID, userID string) error {
syncService := a.Srv().GetSharedChannelSyncService()
if syncService == nil || !syncService.Active() {
return model.NewAppError("InviteRemoteToChannel", "api.command_share.service_disabled",
nil, "", http.StatusBadRequest)
}
hasRemote, err := a.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
return nil
}
// Check if channel is shared or not.
hasChan, err := a.HasSharedChannel(channelID)
if err != nil {
return model.NewAppError("InviteRemoteToChannel", "api.command_share.check_channel_exist.error",
map[string]any{"ChannelID": channelID, "Error": err.Error()}, "", http.StatusInternalServerError)
}
if !hasChan {
return model.NewAppError("InviteRemoteToChannel", "api.command_share.channel_not_shared.error",
map[string]any{"ChannelID": channelID}, "", http.StatusBadRequest)
}
// don't allow invitation to shared channel originating from remote.
// (also blocks cyclic invitations)
if err := a.CheckCanInviteToSharedChannel(channelID); err != nil {
return model.NewAppError("InviteRemoteToChannel", "api.command_share.channel_invite_not_home.error", nil, "", http.StatusInternalServerError)
}
rc, appErr := a.GetRemoteCluster(remoteID)
if appErr != nil {
return model.NewAppError("InviteRemoteToChannel", "api.command_share.remote_id_invalid.error",
map[string]any{"Error": appErr.Error()}, "", http.StatusInternalServerError).Wrap(appErr)
}
channel, errApp := a.GetChannel(request.EmptyContext(a.Log()), channelID)
if errApp != nil {
return model.NewAppError("InviteRemoteToChannel", "api.command_share.channel_invite.error",
map[string]any{"Name": rc.DisplayName, "Error": errApp.Error()}, "", http.StatusInternalServerError).Wrap(appErr)
}
// send channel invite to remote cluster. Will notify clients of channel change.
if err := syncService.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 (a *App) UninviteRemoteFromChannel(channelID, remoteID string) error {
scr, err := a.GetSharedChannelRemoteByIds(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 := a.Srv().Store().SharedChannel().DeleteRemote(scr.Id)
if err != nil || !deleted {
code := http.StatusInternalServerError
if err == nil {
err = errNotFound
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
}
func (a *App) SaveSharedChannelRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
if err := a.checkChannelIsShared(remote.ChannelId); err != nil {
return nil, err
@@ -164,3 +272,66 @@ func (a *App) onUserProfileChange(userID string) {
}
syncService.NotifyUserProfileChanged(userID)
}
// Sync
// UpdateSharedChannelCursor updates the cursor for the specified channelID and remoteID.
// This can be used to manually set the point of last sync, either forward to skip older posts,
// or backward to re-sync history.
// This call by itself does not force a re-sync - a change to channel contents or a call to
// SyncSharedChannel are needed to force a sync.
func (a *App) UpdateSharedChannelCursor(channelID, remoteID string, cursor model.GetPostsSinceForSyncCursor) error {
src, err := a.Srv().Store().SharedChannel().GetRemoteByIds(channelID, remoteID)
if err != nil {
return fmt.Errorf("cursor update failed - cannot fetch shared channel remote: %w", err)
}
return a.Srv().Store().SharedChannel().UpdateRemoteCursor(src.Id, cursor)
}
// SyncSharedChannel forces a shared channel to send any changed content to all remote clusters.
func (a *App) SyncSharedChannel(channelID string) error {
syncService := a.Srv().GetSharedChannelSyncService()
if syncService == nil || !syncService.Active() {
return model.NewAppError("InviteRemoteToChannel", "api.command_share.service_disabled",
nil, "", http.StatusBadRequest)
}
syncService.NotifyChannelChanged(channelID)
return nil
}
// Hooks
var ErrPluginUnavailable = errors.New("plugin unavialable")
// OnSharedChannelsSyncMsg is called by the Shared Channels service for a registered plugin when there is new content
// that needs to be synchronized.
func (a *App) OnSharedChannelsSyncMsg(msg *model.SyncMsg, rc *model.RemoteCluster) (model.SyncResponse, error) {
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return model.SyncResponse{}, fmt.Errorf("cannot deliver sync msg to plugin %s: %w", rc.PluginID, ErrPluginUnavailable)
}
pluginHooks, err := pluginsEnvironment.HooksForPlugin(rc.PluginID)
if err != nil {
return model.SyncResponse{}, fmt.Errorf("cannot deliver sync msg to plugin %s: %w", rc.PluginID, err)
}
return pluginHooks.OnSharedChannelsSyncMsg(msg, rc)
}
// OnSharedChannelsPing is called by the Shared Channels service for a registered plugin wto check that the plugin
// is still responding and has a connection to any upstream services it needs (e.g. MS Graph API).
func (a *App) OnSharedChannelsPing(rc *model.RemoteCluster) bool {
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return false
}
pluginHooks, err := pluginsEnvironment.HooksForPlugin(rc.PluginID)
if err != nil {
return false
}
return pluginHooks.OnSharedChannelsPing(rc)
}

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

@@ -38,7 +38,7 @@ func TestApp_CheckCanInviteToSharedChannel(t *testing.T) {
CreatorId: th.BasicUser.Id,
RemoteId: d.remoteId,
}
_, err := th.App.SaveSharedChannel(th.Context, sc)
_, err := th.App.ShareChannel(th.Context, sc)
require.NoError(t, err)
}

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

@@ -127,11 +127,13 @@ func (sp *ShareProvider) DoCommand(a *app.App, c request.CTX, args *model.Comman
return responsef(args.T("api.command_share.permission_required", map[string]any{"Permission": "manage_shared_channels"}))
}
if a.Srv().GetSharedChannelSyncService() == nil {
syncService := a.Srv().GetSharedChannelSyncService()
if syncService == nil || !syncService.Active() {
return responsef(args.T("api.command_share.service_disabled"))
}
if a.Srv().GetRemoteClusterService() == nil {
rcService := a.Srv().GetRemoteClusterService()
if rcService == nil || !rcService.Active() {
return responsef(args.T("api.command_remote.service_disabled"))
}
@@ -157,7 +159,7 @@ func (sp *ShareProvider) DoCommand(a *app.App, c request.CTX, args *model.Comman
}
func (sp *ShareProvider) doShareChannel(a *app.App, c request.CTX, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
// check that channel exists.
// fetch defaults for missing channel props
channel, errApp := a.GetChannel(c, args.ChannelId)
if errApp != nil {
return responsef(args.T("api.command_share.share_channel.error", map[string]any{"Error": errApp.Error()}))
@@ -196,22 +198,15 @@ func (sp *ShareProvider) doShareChannel(a *app.App, c request.CTX, args *model.C
CreatorId: args.UserId,
}
if _, err := a.SaveSharedChannel(c, sc); err != nil {
if _, err := a.ShareChannel(c, sc); err != nil {
return responsef(args.T("api.command_share.share_channel.error", map[string]any{"Error": err.Error()}))
}
notifyClientsForChannelUpdate(a, sc)
return responsef("##### " + args.T("api.command_share.channel_shared"))
}
func (sp *ShareProvider) doUnshareChannel(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
sc, appErr := a.GetSharedChannel(args.ChannelId)
if appErr != nil {
return responsef(args.T("api.command_share.shared_channel_unshare.error", map[string]any{"Error": appErr.Error()}))
}
deleted, err := a.DeleteSharedChannel(args.ChannelId)
deleted, err := a.UnshareChannel(args.ChannelId)
if err != nil {
return responsef(args.T("api.command_share.shared_channel_unshare.error", map[string]any{"Error": err.Error()}))
}
@@ -219,8 +214,6 @@ func (sp *ShareProvider) doUnshareChannel(a *app.App, args *model.CommandArgs, m
return responsef(args.T("api.command_share.not_shared_channel_unshare"))
}
notifyClientsForChannelUpdate(a, sc)
return responsef("##### " + args.T("api.command_share.shared_channel_unavailable"))
}
@@ -241,7 +234,7 @@ func (sp *ShareProvider) doInviteRemote(a *app.App, c request.CTX, args *model.C
// Check if channel is shared or not.
hasChan, err := a.HasSharedChannel(args.ChannelId)
if err != nil {
return responsef(args.T("api.command_share.check_channel_exist.error", map[string]any{"Error": err.Error()}))
return responsef(args.T("api.command_share.check_channel_exist.error", map[string]any{"ChannelID": args.ChannelId, "Error": err.Error()}))
}
if !hasChan {
// If it doesn't exist, then create it.
@@ -253,24 +246,13 @@ func (sp *ShareProvider) doInviteRemote(a *app.App, c request.CTX, args *model.C
}()
}
// don't allow invitation to shared channel originating from remote.
// (also blocks cyclic invitations)
if err := a.CheckCanInviteToSharedChannel(args.ChannelId); err != nil {
return responsef(args.T("api.command_share.channel_invite_not_home.error"))
}
rc, appErr := a.GetRemoteCluster(remoteID)
if appErr != nil {
return responsef(args.T("api.command_share.remote_id_invalid.error", map[string]any{"Error": appErr.Error()}))
}
channel, errApp := a.GetChannel(c, args.ChannelId)
if errApp != nil {
return responsef(args.T("api.command_share.channel_invite.error", map[string]any{"Name": rc.DisplayName, "Error": errApp.Error()}))
}
// send channel invite to remote cluster
if err := a.Srv().GetSharedChannelSyncService().SendChannelInvite(channel, args.UserId, rc); err != nil {
return responsef(args.T("api.command_share.channel_invite.error", map[string]any{"Name": rc.DisplayName, "Error": err.Error()}))
if err = a.InviteRemoteToChannel(args.ChannelId, remoteID, args.UserId); err != nil {
return responsef(appErr.Error())
}
return responsef("##### " + args.T("api.command_share.invitation_sent", map[string]any{"Name": rc.DisplayName, "SiteURL": rc.SiteURL}))
@@ -282,15 +264,11 @@ func (sp *ShareProvider) doUninviteRemote(a *app.App, args *model.CommandArgs, m
return responsef(args.T("api.command_share.remote_not_valid"))
}
scr, err := a.GetSharedChannelRemoteByIds(args.ChannelId, remoteID)
if err != nil || scr.ChannelId != args.ChannelId {
return responsef(args.T("api.command_share.channel_remote_id_not_exists", map[string]any{"RemoteId": remoteID}))
err := a.UninviteRemoteFromChannel(args.ChannelId, remoteID)
if err != nil {
return responsef(err.Error())
}
deleted, err := a.DeleteSharedChannelRemote(scr.Id)
if err != nil || !deleted {
return responsef(args.T("api.command_share.could_not_uninvite.error", map[string]any{"RemoteId": remoteID, "Error": err.Error()}))
}
return responsef("##### " + args.T("api.command_share.remote_uninvited", map[string]any{"RemoteId": remoteID}))
}
@@ -323,9 +301,3 @@ func (sp *ShareProvider) doStatus(a *app.App, args *model.CommandArgs, _ map[str
}
return responsef(sb.String())
}
func notifyClientsForChannelUpdate(a *app.App, sharedChannel *model.SharedChannel) {
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelConverted, sharedChannel.TeamId, "", "", nil, "")
messageWs.Add("channel_id", sharedChannel.ChannelId)
a.Publish(messageWs)
}

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

@@ -26,15 +26,19 @@ func TestShareProviderDoCommand(t *testing.T) {
th.addPermissionToRole(model.PermissionManageSharedChannels.Id, th.BasicUser.Roles)
mockSyncService := app.NewMockSharedChannelService(nil)
mockSyncService := app.NewMockSharedChannelService(nil, app.MockOptionSharedChannelServiceWithActive(true))
th.Server.SetSharedChannelSyncService(mockSyncService)
mockRemoteCluster, err := remotecluster.NewRemoteClusterService(th.Server)
remoteClusterService, err := remotecluster.NewRemoteClusterService(th.Server, th.App)
require.NoError(t, err)
th.Server.SetRemoteClusterService(mockRemoteCluster)
th.Server.SetRemoteClusterService(remoteClusterService)
testCluster := &testlib.FakeClusterInterface{}
th.Server.Platform().SetCluster(testCluster)
err = remoteClusterService.Start()
require.NoError(t, err)
defer remoteClusterService.Shutdown()
commandProvider := ShareProvider{}
channel := th.CreateChannel(th.BasicTeam, WithShared(false))
@@ -53,7 +57,7 @@ func TestShareProviderDoCommand(t *testing.T) {
event, err := model.WebSocketEventFromJSON(bytes.NewReader(msg.Data))
return err == nil && event.EventType() == model.WebsocketEventChannelConverted
})
assert.Len(t, channelConvertedMessages, 1)
assert.Len(t, channelConvertedMessages, 1) // one msg for share creation
})
t.Run("unshare command sends a websocket channel converted event", func(t *testing.T) {
@@ -64,13 +68,17 @@ func TestShareProviderDoCommand(t *testing.T) {
mockSyncService := app.NewMockSharedChannelService(nil)
th.Server.SetSharedChannelSyncService(mockSyncService)
mockRemoteCluster, err := remotecluster.NewRemoteClusterService(th.Server)
remoteClusterService, err := remotecluster.NewRemoteClusterService(th.Server, th.App)
require.NoError(t, err)
th.Server.SetRemoteClusterService(mockRemoteCluster)
th.Server.SetRemoteClusterService(remoteClusterService)
testCluster := &testlib.FakeClusterInterface{}
th.Server.Platform().SetCluster(testCluster)
err = remoteClusterService.Start()
require.NoError(t, err)
defer remoteClusterService.Shutdown()
commandProvider := ShareProvider{}
channel := th.CreateChannel(th.BasicTeam, WithShared(true))
args := &model.CommandArgs{
@@ -88,6 +96,6 @@ func TestShareProviderDoCommand(t *testing.T) {
event, err := model.WebSocketEventFromJSON(bytes.NewReader(msg.Data))
return err == nil && event.EventType() == model.WebsocketEventChannelConverted
})
require.Len(t, channelConvertedMessages, 1)
require.Len(t, channelConvertedMessages, 2) // one msg for share creation, one for unshare.
})
}

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

@@ -274,7 +274,7 @@ func (th *TestHelper) createChannel(team *model.Team, channelType model.ChannelT
if channel.IsShared() {
id := model.NewId()
_, err := th.App.SaveSharedChannel(th.Context, &model.SharedChannel{
_, err := th.App.ShareChannel(th.Context, &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
Home: false,

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

@@ -7643,6 +7643,24 @@ func (s *OpenTracingLayerRemoteClusterStore) GetAll(filter model.RemoteClusterQu
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) GetByPluginID(pluginID string) (*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.GetByPluginID")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RemoteClusterStore.GetByPluginID(pluginID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) Save(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.Save")

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

@@ -8684,6 +8684,27 @@ func (s *RetryLayerRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFil
}
func (s *RetryLayerRemoteClusterStore) GetByPluginID(pluginID string) (*model.RemoteCluster, error) {
tries := 0
for {
result, err := s.RemoteClusterStore.GetByPluginID(pluginID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) Save(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
tries := 0

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

@@ -4,6 +4,7 @@
package sqlstore
import (
"database/sql"
"fmt"
"strings"
@@ -50,6 +51,19 @@ func (s sqlRemoteClusterStore) Save(remoteCluster *model.RemoteCluster) (*model.
return nil, err
}
// check for pluginID collisions - on collision treat as idempotent
if remoteCluster.PluginID != "" {
rc, err := s.GetByPluginID(remoteCluster.PluginID)
if err == nil {
// if this plugin id already exists, just return it
return rc, nil
}
if !errors.Is(err, sql.ErrNoRows) {
// anything other than NotFound is unexpected
return nil, errors.Wrapf(err, "failed to lookup RemoteCluster by pluginID %s", remoteCluster.PluginID)
}
}
query := `INSERT INTO RemoteClusters
(RemoteId, RemoteTeamId, Name, DisplayName, SiteURL, CreateAt,
LastPingAt, Token, RemoteToken, Topics, CreatorId, PluginID, Options)
@@ -69,6 +83,7 @@ func (s sqlRemoteClusterStore) Update(remoteCluster *model.RemoteCluster) (*mode
return nil, err
}
// not all fields can be updated.
query := `UPDATE RemoteClusters
SET Token = :Token,
RemoteTeamId = :RemoteTeamId,
@@ -129,6 +144,24 @@ func (s sqlRemoteClusterStore) Get(remoteId string) (*model.RemoteCluster, error
return &rc, nil
}
func (s sqlRemoteClusterStore) GetByPluginID(pluginID string) (*model.RemoteCluster, error) {
query := s.getQueryBuilder().
Select(remoteClusterFields("")...).
From("RemoteClusters").
Where(sq.Eq{"PluginID": pluginID})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "remote_cluster_get_by_pluginid_tosql")
}
var rc model.RemoteCluster
if err := s.GetReplicaX().Get(&rc, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find RemoteCluster by plugin_id")
}
return &rc, nil
}
func (s sqlRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
query := s.getQueryBuilder().
Select(remoteClusterFields("rc")...).

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

@@ -543,6 +543,7 @@ type RemoteClusterStore interface {
Update(rc *model.RemoteCluster) (*model.RemoteCluster, error)
Delete(remoteClusterId string) (bool, error)
Get(remoteClusterId string) (*model.RemoteCluster, error)
GetByPluginID(pluginID string) (*model.RemoteCluster, error)
GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error)
UpdateTopics(remoteClusterId string, topics string) (*model.RemoteCluster, error)
SetLastPingAt(remoteClusterId string) error

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

@@ -90,6 +90,32 @@ func (_m *RemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*
return r0, r1
}
// GetByPluginID provides a mock function with given fields: pluginID
func (_m *RemoteClusterStore) GetByPluginID(pluginID string) (*model.RemoteCluster, error) {
ret := _m.Called(pluginID)
var r0 *model.RemoteCluster
var r1 error
if rf, ok := ret.Get(0).(func(string) (*model.RemoteCluster, error)); ok {
return rf(pluginID)
}
if rf, ok := ret.Get(0).(func(string) *model.RemoteCluster); ok {
r0 = rf(pluginID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RemoteCluster)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(pluginID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: rc
func (_m *RemoteClusterStore) Save(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
ret := _m.Called(rc)

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

@@ -15,10 +15,6 @@ import (
"github.com/stretchr/testify/require"
)
const (
testPluginID = "com.sample.blap"
)
func TestRemoteClusterStore(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("RemoteClusterGetAllInChannel", func(t *testing.T) { testRemoteClusterGetAllInChannel(t, rctx, ss) })
t.Run("RemoteClusterGetAllNotInChannel", func(t *testing.T) { testRemoteClusterGetAllNotInChannel(t, rctx, ss) })
@@ -30,12 +26,17 @@ func TestRemoteClusterStore(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("RemoteClusterUpdateTopics", func(t *testing.T) { testRemoteClusterUpdateTopics(t, rctx, ss) })
}
func makeSiteURL() string {
return "www.example.com/" + model.NewId()
}
func testRemoteClusterSave(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("Save", func(t *testing.T) {
rc := &model.RemoteCluster{
Name: "some_remote",
SiteURL: "somewhere.com",
SiteURL: makeSiteURL(),
CreatorId: model.NewId(),
PluginID: model.NewId(),
}
rcSaved, err := ss.RemoteCluster().Save(rc)
@@ -44,12 +45,13 @@ func testRemoteClusterSave(t *testing.T, rctx request.CTX, ss store.Store) {
require.Equal(t, rc.SiteURL, rcSaved.SiteURL)
require.Greater(t, rc.CreateAt, int64(0))
require.Equal(t, rc.LastPingAt, int64(0))
require.Equal(t, rc.PluginID, rcSaved.PluginID)
require.Equal(t, rc.Options, model.Bitmask(0))
})
t.Run("Save missing display name", func(t *testing.T) {
rc := &model.RemoteCluster{
SiteURL: "somewhere.com",
SiteURL: makeSiteURL(),
CreatorId: model.NewId(),
}
_, err := ss.RemoteCluster().Save(rc)
@@ -59,34 +61,84 @@ func testRemoteClusterSave(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("Save missing creator id", func(t *testing.T) {
rc := &model.RemoteCluster{
Name: "some_remote_2",
SiteURL: "somewhere.com",
SiteURL: makeSiteURL(),
}
_, err := ss.RemoteCluster().Save(rc)
require.Error(t, err)
})
t.Run("Save pluginID collision", func(t *testing.T) {
const testPluginID = "com.example.collision"
rc := &model.RemoteCluster{
Name: "some_remote",
SiteURL: makeSiteURL(),
CreatorId: model.NewId(),
PluginID: testPluginID,
}
_, err := ss.RemoteCluster().Save(rc)
require.NoError(t, err)
rc2 := &model.RemoteCluster{
Name: "another_remote",
SiteURL: makeSiteURL(),
CreatorId: model.NewId(),
PluginID: testPluginID,
}
rcSaved, err := ss.RemoteCluster().Save(rc2)
require.NoError(t, err)
require.NotNil(t, rcSaved)
// original remotecluster should be returned
require.Equal(t, rc.Name, rcSaved.Name)
require.Equal(t, rc.SiteURL, rcSaved.SiteURL)
require.Greater(t, rc.CreateAt, int64(0))
require.Equal(t, rc.PluginID, rcSaved.PluginID)
})
t.Run("Save multiple with blank pluginID", func(t *testing.T) {
rc := &model.RemoteCluster{
Name: model.NewId(),
SiteURL: makeSiteURL(),
CreatorId: model.NewId(),
}
_, err := ss.RemoteCluster().Save(rc)
require.NoError(t, err)
rc2 := &model.RemoteCluster{
Name: model.NewId(),
SiteURL: makeSiteURL(),
CreatorId: model.NewId(),
}
_, err = ss.RemoteCluster().Save(rc2)
require.NoError(t, err)
})
t.Run("Save for plugin with options", func(t *testing.T) {
rc := &model.RemoteCluster{
Name: "plugin_remote",
SiteURL: "plugin.example.com",
SiteURL: makeSiteURL(),
CreatorId: model.NewId(),
PluginID: testPluginID,
PluginID: model.NewId(),
Options: model.BitflagOptionAutoShareDMs,
}
rcSaved, err := ss.RemoteCluster().Save(rc)
require.NoError(t, err)
require.Equal(t, testPluginID, rcSaved.PluginID)
require.Equal(t, rc.PluginID, rcSaved.PluginID)
require.Equal(t, model.BitflagOptionAutoShareDMs, rcSaved.Options)
require.True(t, rcSaved.IsOptionFlagSet(model.BitflagOptionAutoShareDMs))
rc.Name = "plugin_remote_2"
rc.SiteURL = makeSiteURL()
rc.PluginID = model.NewId()
rc.SiteURL = "plugin2.example.com"
rc.UnsetOptionFlag(model.BitflagOptionAutoShareDMs)
rcSaved, err = ss.RemoteCluster().Save(rc)
require.NoError(t, err)
require.Equal(t, testPluginID, rcSaved.PluginID)
require.Equal(t, rc.PluginID, rcSaved.PluginID)
require.Equal(t, model.Bitmask(0), rcSaved.Options)
require.False(t, rcSaved.IsOptionFlagSet(model.BitflagOptionAutoShareDMs))
})
@@ -96,7 +148,7 @@ func testRemoteClusterDelete(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("Delete", func(t *testing.T) {
rc := &model.RemoteCluster{
Name: "shortlived_remote",
SiteURL: "nowhere.com",
SiteURL: makeSiteURL(),
CreatorId: model.NewId(),
}
rcSaved, err := ss.RemoteCluster().Save(rc)
@@ -118,9 +170,9 @@ func testRemoteClusterGet(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("Get", func(t *testing.T) {
rc := &model.RemoteCluster{
Name: "shortlived_remote_2",
SiteURL: "nowhere.com",
SiteURL: makeSiteURL(),
CreatorId: model.NewId(),
PluginID: testPluginID,
PluginID: model.NewId(),
}
rc.SetOptionFlag(model.BitflagOptionAutoShareDMs)
rcSaved, err := ss.RemoteCluster().Save(rc)
@@ -129,7 +181,7 @@ func testRemoteClusterGet(t *testing.T, rctx request.CTX, ss store.Store) {
rcGet, err := ss.RemoteCluster().Get(rcSaved.RemoteId)
require.NoError(t, err)
require.Equal(t, rcSaved.RemoteId, rcGet.RemoteId)
require.Equal(t, testPluginID, rcGet.PluginID)
require.Equal(t, rcSaved.PluginID, rcGet.PluginID)
require.True(t, rcGet.IsOptionFlagSet(model.BitflagOptionAutoShareDMs))
})
@@ -147,10 +199,10 @@ func testRemoteClusterGetAll(t *testing.T, rctx request.CTX, ss store.Store) {
pingLongAgo := model.GetMillis() - (model.RemoteOfflineAfterMillis * 3)
data := []*model.RemoteCluster{
{Name: "offline_remote", CreatorId: userId, SiteURL: "somewhere.com", LastPingAt: pingLongAgo, Topics: " shared incident "},
{Name: "some_online_remote", CreatorId: userId, SiteURL: "nowhere.com", LastPingAt: now, Topics: " shared incident "},
{Name: "another_online_remote", CreatorId: model.NewId(), SiteURL: "underwhere.com", LastPingAt: now, Topics: ""},
{Name: "another_offline_remote", CreatorId: model.NewId(), SiteURL: "knowhere.com", LastPingAt: pingLongAgo, Topics: " shared "},
{Name: "offline_remote", CreatorId: userId, SiteURL: makeSiteURL(), LastPingAt: pingLongAgo, Topics: " shared incident "},
{Name: "some_online_remote", CreatorId: userId, SiteURL: makeSiteURL(), LastPingAt: now, Topics: " shared incident "},
{Name: "another_online_remote", CreatorId: model.NewId(), SiteURL: makeSiteURL(), LastPingAt: now, Topics: ""},
{Name: "another_offline_remote", CreatorId: model.NewId(), SiteURL: makeSiteURL(), LastPingAt: pingLongAgo, Topics: " shared "},
{Name: "brand_new_offline_remote", CreatorId: userId, SiteURL: "", LastPingAt: 0, Topics: " bogus shared stuff "},
}
@@ -245,6 +297,11 @@ func testRemoteClusterGetAll(t *testing.T, rctx request.CTX, ss store.Store) {
}
func testRemoteClusterGetAllInChannel(t *testing.T, rctx request.CTX, ss store.Store) {
const (
testPluginID_1 = "com.sample.blap"
testPluginID_2 = "com.sample.bloop"
)
require.NoError(t, clearRemoteClusters(ss))
now := model.GetMillis()
@@ -272,8 +329,8 @@ func testRemoteClusterGetAllInChannel(t *testing.T, rctx request.CTX, ss store.S
// Create some remote clusters
rcData := []*model.RemoteCluster{
{Name: "AAAA_Inc", CreatorId: userId, SiteURL: "aaaa.com", RemoteId: model.NewId(), LastPingAt: now, PluginID: testPluginID},
{Name: "BBBB_Inc", CreatorId: userId, SiteURL: "bbbb.com", RemoteId: model.NewId(), LastPingAt: 0, PluginID: testPluginID},
{Name: "AAAA_Inc", CreatorId: userId, SiteURL: "aaaa.com", RemoteId: model.NewId(), LastPingAt: now, PluginID: testPluginID_1},
{Name: "BBBB_Inc", CreatorId: userId, SiteURL: "bbbb.com", RemoteId: model.NewId(), LastPingAt: 0, PluginID: testPluginID_2},
{Name: "CCCC_Inc", CreatorId: userId, SiteURL: "cccc.com", RemoteId: model.NewId(), LastPingAt: now},
{Name: "DDDD_Inc", CreatorId: userId, SiteURL: "dddd.com", RemoteId: model.NewId(), LastPingAt: now},
{Name: "EEEE_Inc", CreatorId: userId, SiteURL: "eeee.com", RemoteId: model.NewId(), LastPingAt: 0},
@@ -305,8 +362,8 @@ func testRemoteClusterGetAllInChannel(t *testing.T, rctx request.CTX, ss store.S
require.Len(t, list, 2, "channel 1 should have 2 remote clusters")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[0].RemoteId, rcData[1].RemoteId}, ids)
require.Equal(t, testPluginID, rcData[0].PluginID)
require.Equal(t, testPluginID, rcData[1].PluginID)
require.Equal(t, testPluginID_1, rcData[0].PluginID)
require.Equal(t, testPluginID_2, rcData[1].PluginID)
})
t.Run("Channel 1 online only", func(t *testing.T) {

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

@@ -6905,6 +6905,22 @@ func (s *TimerLayerRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFil
return result, err
}
func (s *TimerLayerRemoteClusterStore) GetByPluginID(pluginID string) (*model.RemoteCluster, error) {
start := time.Now()
result, err := s.RemoteClusterStore.GetByPluginID(pluginID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.GetByPluginID", success, elapsed)
}
return result, err
}
func (s *TimerLayerRemoteClusterStore) Save(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
start := time.Now()