MM-27493 Shared channels (MVP) (#17301)
Remote Cluster Service - provides ability for multiple Mattermost cluster instances to create a trusted connection with each other and exchange messages - trusted connections are managed via slash commands (for now) - facilitates features requiring inter-cluster communication, such as Shared Channels Shared Channels Service - provides ability to shared channels between one or more Mattermost cluster instances (using trusted connection) - sharing/unsharing of channels is managed via slash commands (for now)
Этот коммит содержится в:
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v5/plugin"
|
||||
"github.com/mattermost/mattermost-server/v5/services/httpservice"
|
||||
"github.com/mattermost/mattermost-server/v5/services/imageproxy"
|
||||
"github.com/mattermost/mattermost-server/v5/services/remotecluster"
|
||||
"github.com/mattermost/mattermost-server/v5/services/searchengine"
|
||||
"github.com/mattermost/mattermost-server/v5/services/timezones"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/filestore"
|
||||
@@ -199,6 +200,8 @@ type AppIface interface {
|
||||
GetTeamSchemeChannelRoles(teamID string) (guestRoleName string, userRoleName string, adminRoleName string, err *model.AppError)
|
||||
// GetTotalUsersStats is used for the DM list total
|
||||
GetTotalUsersStats(viewRestrictions *model.ViewUsersRestrictions) (*model.UsersStats, *model.AppError)
|
||||
// HasRemote returns whether a given channelID is present in the channel remotes or not.
|
||||
HasRemote(channelID string, remoteID string) (bool, error)
|
||||
// HubRegister registers a connection to a hub.
|
||||
HubRegister(webConn *WebConn)
|
||||
// HubStart starts all the hubs.
|
||||
@@ -361,6 +364,7 @@ type AppIface interface {
|
||||
AddDirectChannels(teamID string, user *model.User) *model.AppError
|
||||
AddLdapPrivateCertificate(fileData *multipart.FileHeader) *model.AppError
|
||||
AddLdapPublicCertificate(fileData *multipart.FileHeader) *model.AppError
|
||||
AddRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError)
|
||||
AddSamlIdpCertificate(fileData *multipart.FileHeader) *model.AppError
|
||||
AddSamlPrivateCertificate(fileData *multipart.FileHeader) *model.AppError
|
||||
AddSamlPublicCertificate(fileData *multipart.FileHeader) *model.AppError
|
||||
@@ -399,6 +403,7 @@ type AppIface interface {
|
||||
ChannelMembersToAdd(since int64, channelID *string) ([]*model.UserChannelIDPair, *model.AppError)
|
||||
ChannelMembersToRemove(teamID *string) ([]*model.ChannelMember, *model.AppError)
|
||||
CheckAndSendUserLimitWarningEmails() *model.AppError
|
||||
CheckCanInviteToSharedChannel(channelId string) error
|
||||
CheckForClientSideCert(r *http.Request) (string, string, string)
|
||||
CheckMandatoryS3Fields(settings *model.FileSettings) *model.AppError
|
||||
CheckPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError
|
||||
@@ -483,7 +488,10 @@ type AppIface interface {
|
||||
DeletePostFiles(post *model.Post)
|
||||
DeletePreferences(userID string, preferences model.Preferences) *model.AppError
|
||||
DeleteReactionForPost(reaction *model.Reaction) *model.AppError
|
||||
DeleteRemoteCluster(remoteClusterId string) (bool, *model.AppError)
|
||||
DeleteScheme(schemeId string) (*model.Scheme, *model.AppError)
|
||||
DeleteSharedChannel(channelID string) (bool, error)
|
||||
DeleteSharedChannelRemote(id string) (bool, error)
|
||||
DeleteSidebarCategory(userID, teamID, categoryId string) *model.AppError
|
||||
DeleteToken(token *model.Token) *model.AppError
|
||||
DisableAutoResponder(userID string, asAdmin bool) *model.AppError
|
||||
@@ -524,6 +532,7 @@ type AppIface interface {
|
||||
GetAllPublicTeams() ([]*model.Team, *model.AppError)
|
||||
GetAllPublicTeamsPage(offset int, limit int) ([]*model.Team, *model.AppError)
|
||||
GetAllPublicTeamsPageWithCount(offset int, limit int) (*model.TeamsWithCount, *model.AppError)
|
||||
GetAllRemoteClusters(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, *model.AppError)
|
||||
GetAllRoles() ([]*model.Role, *model.AppError)
|
||||
GetAllStatuses() map[string]*model.Status
|
||||
GetAllTeams() ([]*model.Team, *model.AppError)
|
||||
@@ -626,7 +635,7 @@ type AppIface interface {
|
||||
GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string) (string, *model.AppError)
|
||||
GetOAuthStateToken(token string) (*model.Token, *model.AppError)
|
||||
GetOpenGraphMetadata(requestURL string) *opengraph.OpenGraph
|
||||
GetOrCreateDirectChannel(userID, otherUserID string) (*model.Channel, *model.AppError)
|
||||
GetOrCreateDirectChannel(userID, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError)
|
||||
GetOutgoingWebhook(hookID string) (*model.OutgoingWebhook, *model.AppError)
|
||||
GetOutgoingWebhooksForChannelPageByUser(channelID string, userID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError)
|
||||
GetOutgoingWebhooksForTeamPage(teamID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError)
|
||||
@@ -661,6 +670,10 @@ type AppIface interface {
|
||||
GetReactionsForPost(postID string) ([]*model.Reaction, *model.AppError)
|
||||
GetRecentlyActiveUsersForTeam(teamID string) (map[string]*model.User, *model.AppError)
|
||||
GetRecentlyActiveUsersForTeamPage(teamID string, page, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError)
|
||||
GetRemoteCluster(remoteClusterId string) (*model.RemoteCluster, *model.AppError)
|
||||
GetRemoteClusterForUser(remoteID string, userID string) (*model.RemoteCluster, *model.AppError)
|
||||
GetRemoteClusterService() (remotecluster.RemoteClusterServiceIFace, *model.AppError)
|
||||
GetRemoteClusterSession(token string, remoteId string) (*model.Session, *model.AppError)
|
||||
GetRole(id string) (*model.Role, *model.AppError)
|
||||
GetRoleByName(name string) (*model.Role, *model.AppError)
|
||||
GetRolesByNames(names []string) ([]*model.Role, *model.AppError)
|
||||
@@ -676,6 +689,13 @@ type AppIface interface {
|
||||
GetSession(token string) (*model.Session, *model.AppError)
|
||||
GetSessionById(sessionID string) (*model.Session, *model.AppError)
|
||||
GetSessions(userID string) ([]*model.Session, *model.AppError)
|
||||
GetSharedChannel(channelID string) (*model.SharedChannel, error)
|
||||
GetSharedChannelRemote(id string) (*model.SharedChannelRemote, error)
|
||||
GetSharedChannelRemoteByIds(channelID string, remoteID string) (*model.SharedChannelRemote, error)
|
||||
GetSharedChannelRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error)
|
||||
GetSharedChannelRemotesStatus(channelID string) ([]*model.SharedChannelRemoteStatus, error)
|
||||
GetSharedChannels(page int, perPage int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, *model.AppError)
|
||||
GetSharedChannelsCount(opts model.SharedChannelFilterOpts) (int64, error)
|
||||
GetSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError)
|
||||
GetSidebarCategory(categoryId string) (*model.SidebarCategoryWithChannels, *model.AppError)
|
||||
GetSidebarCategoryOrder(userID, teamID string) ([]string, *model.AppError)
|
||||
@@ -754,6 +774,7 @@ type AppIface interface {
|
||||
HasPermissionToChannelByPost(askingUserId string, postID string, permission *model.Permission) bool
|
||||
HasPermissionToTeam(askingUserId string, teamID string, permission *model.Permission) bool
|
||||
HasPermissionToUser(askingUserId string, userID string) bool
|
||||
HasSharedChannel(channelID string) (bool, error)
|
||||
HubStop()
|
||||
ImageProxy() *imageproxy.ImageProxy
|
||||
ImageProxyAdder() func(string) string
|
||||
@@ -884,6 +905,8 @@ type AppIface interface {
|
||||
SaveBrandImage(imageData *multipart.FileHeader) *model.AppError
|
||||
SaveComplianceReport(job *model.Compliance) (*model.Compliance, *model.AppError)
|
||||
SaveReactionForPost(reaction *model.Reaction) (*model.Reaction, *model.AppError)
|
||||
SaveSharedChannel(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
|
||||
SearchArchivedChannels(teamID string, term string, userID string) (*model.ChannelList, *model.AppError)
|
||||
@@ -942,6 +965,7 @@ type AppIface interface {
|
||||
SetProfileImage(userID string, imageData *multipart.FileHeader) *model.AppError
|
||||
SetProfileImageFromFile(userID string, file io.Reader) *model.AppError
|
||||
SetProfileImageFromMultiPartFile(userID string, file multipart.File) *model.AppError
|
||||
SetRemoteClusterLastPingAt(remoteClusterId string) *model.AppError
|
||||
SetRequestId(s string)
|
||||
SetSamlIdpCertificateFromMetadata(data []byte) *model.AppError
|
||||
SetSearchEngine(se *searchengine.Broker)
|
||||
@@ -1009,9 +1033,13 @@ type AppIface interface {
|
||||
UpdatePasswordSendEmail(user *model.User, newPassword, method string) *model.AppError
|
||||
UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model.AppError)
|
||||
UpdatePreferences(userID string, preferences model.Preferences) *model.AppError
|
||||
UpdateRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError)
|
||||
UpdateRemoteClusterTopics(remoteClusterId string, topics string) (*model.RemoteCluster, *model.AppError)
|
||||
UpdateRole(role *model.Role) (*model.Role, *model.AppError)
|
||||
UpdateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError)
|
||||
UpdateSessionsIsGuest(userID string, isGuest bool)
|
||||
UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error)
|
||||
UpdateSharedChannelRemoteNextSyncAt(id string, syncTime int64) error
|
||||
UpdateSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError)
|
||||
UpdateSidebarCategoryOrder(userID, teamID string, categoryOrder []string) *model.AppError
|
||||
UpdateTeam(team *model.Team) (*model.Team, *model.AppError)
|
||||
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
TokenLocationCookie
|
||||
TokenLocationQueryString
|
||||
TokenLocationCloudHeader
|
||||
TokenLocationRemoteClusterHeader
|
||||
)
|
||||
|
||||
func (tl TokenLocation) String() string {
|
||||
@@ -34,6 +35,8 @@ func (tl TokenLocation) String() string {
|
||||
return "QueryString"
|
||||
case TokenLocationCloudHeader:
|
||||
return "CloudHeader"
|
||||
case TokenLocationRemoteClusterHeader:
|
||||
return "RemoteClusterHeader"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
@@ -291,5 +294,9 @@ func ParseAuthTokenFromRequest(r *http.Request) (string, TokenLocation) {
|
||||
return token, TokenLocationCloudHeader
|
||||
}
|
||||
|
||||
if token := r.Header.Get(model.HEADER_REMOTECLUSTER_TOKEN); token != "" {
|
||||
return token, TokenLocationRemoteClusterHeader
|
||||
}
|
||||
|
||||
return "", TokenLocationNotFound
|
||||
}
|
||||
|
||||
@@ -125,7 +125,6 @@ func (a *App) JoinDefaultChannels(teamID string, user *model.User, shouldBeAdmin
|
||||
message.Add("user_id", user.Id)
|
||||
message.Add("team_id", channel.TeamId)
|
||||
a.Publish(message)
|
||||
|
||||
}
|
||||
|
||||
if nErr != nil {
|
||||
@@ -322,7 +321,7 @@ func (a *App) CreateChannel(channel *model.Channel, addMember bool) (*model.Chan
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
func (a *App) GetOrCreateDirectChannel(userID, otherUserID string) (*model.Channel, *model.AppError) {
|
||||
func (a *App) GetOrCreateDirectChannel(userID, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
||||
channel, nErr := a.getDirectChannel(userID, otherUserID)
|
||||
if nErr != nil {
|
||||
return nil, nErr
|
||||
@@ -332,7 +331,7 @@ func (a *App) GetOrCreateDirectChannel(userID, otherUserID string) (*model.Chann
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
channel, err := a.createDirectChannel(userID, otherUserID)
|
||||
channel, err := a.createDirectChannel(userID, otherUserID, channelOptions...)
|
||||
if err != nil {
|
||||
if err.Id == store.ChannelExistsError {
|
||||
return channel, nil
|
||||
@@ -381,11 +380,12 @@ func (a *App) handleCreationEvent(userID, otherUserID string, channel *model.Cha
|
||||
}
|
||||
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil)
|
||||
message.Add("creator_id", userID)
|
||||
message.Add("teammate_id", otherUserID)
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
func (a *App) createDirectChannel(userID, otherUserID string) (*model.Channel, *model.AppError) {
|
||||
func (a *App) createDirectChannel(userID string, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
||||
users, err := a.Srv().Store.User().GetMany(context.Background(), []string{userID, otherUserID})
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
@@ -415,11 +415,11 @@ func (a *App) createDirectChannel(userID, otherUserID string) (*model.Channel, *
|
||||
user = users[1]
|
||||
otherUser = users[0]
|
||||
}
|
||||
return a.createDirectChannelWithUser(user, otherUser)
|
||||
return a.createDirectChannelWithUser(user, otherUser, channelOptions...)
|
||||
}
|
||||
|
||||
func (a *App) createDirectChannelWithUser(user, otherUser *model.User) (*model.Channel, *model.AppError) {
|
||||
channel, nErr := a.Srv().Store.Channel().CreateDirectChannel(user, otherUser)
|
||||
func (a *App) createDirectChannelWithUser(user, otherUser *model.User, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
||||
channel, nErr := a.Srv().Store.Channel().CreateDirectChannel(user, otherUser, channelOptions...)
|
||||
if nErr != nil {
|
||||
var invErr *store.ErrInvalidInput
|
||||
var cErr *store.ErrConflict
|
||||
@@ -460,6 +460,27 @@ func (a *App) createDirectChannelWithUser(user, otherUser *model.User) (*model.C
|
||||
}
|
||||
}
|
||||
|
||||
// When the newly created channel is shared and the creator is local
|
||||
// create a local shared channel record
|
||||
if channel.IsShared() && !user.IsRemote() {
|
||||
sc := &model.SharedChannel{
|
||||
ChannelId: channel.Id,
|
||||
TeamId: channel.TeamId,
|
||||
Home: true,
|
||||
ReadOnly: false,
|
||||
ShareName: channel.Name,
|
||||
ShareDisplayName: channel.DisplayName,
|
||||
SharePurpose: channel.Purpose,
|
||||
ShareHeader: channel.Header,
|
||||
CreatorId: user.Id,
|
||||
Type: channel.Type,
|
||||
}
|
||||
|
||||
if _, err := a.SaveSharedChannel(sc); err != nil {
|
||||
return nil, model.NewAppError("CreateDirectChannel", "app.sharedchannel.dm_channel_creation.internal_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -617,7 +617,7 @@ func TestDynamicListArgsForBuiltin(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
provider := &testProvider{}
|
||||
provider := &testCommandProvider{}
|
||||
RegisterCommandProvider(provider)
|
||||
|
||||
command := provider.GetCommand(th.App, nil)
|
||||
@@ -633,18 +633,18 @@ func TestDynamicListArgsForBuiltin(t *testing.T) {
|
||||
|
||||
t.Run("GetAutoCompleteListItems bad arg", func(t *testing.T) {
|
||||
suggestions := th.App.getSuggestions(emptyCmdArgs, []*model.AutocompleteData{command.AutocompleteData}, "", "bogus --badArg ", model.SYSTEM_ADMIN_ROLE_ID)
|
||||
assert.Len(t, suggestions, 0)
|
||||
assert.Empty(t, suggestions)
|
||||
})
|
||||
}
|
||||
|
||||
type testProvider struct {
|
||||
type testCommandProvider struct {
|
||||
}
|
||||
|
||||
func (p *testProvider) GetTrigger() string {
|
||||
func (p *testCommandProvider) GetTrigger() string {
|
||||
return "bogus"
|
||||
}
|
||||
|
||||
func (p *testProvider) GetCommand(a *App, T i18n.TranslateFunc) *model.Command {
|
||||
func (p *testCommandProvider) GetCommand(a *App, T i18n.TranslateFunc) *model.Command {
|
||||
top := model.NewAutocompleteData(p.GetTrigger(), "[command]", "Just a test.")
|
||||
top.AddNamedDynamicListArgument("dynaArg", "A dynamic list", "builtin:bogus", true)
|
||||
|
||||
@@ -658,14 +658,14 @@ func (p *testProvider) GetCommand(a *App, T i18n.TranslateFunc) *model.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *testProvider) DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse {
|
||||
func (p *testCommandProvider) DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse {
|
||||
return &model.CommandResponse{
|
||||
Text: "I do nothing!",
|
||||
ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *testProvider) GetAutoCompleteListItems(a *App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
|
||||
func (p *testCommandProvider) GetAutoCompleteListItems(a *App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
|
||||
if arg.Name == "dynaArg" {
|
||||
return []model.AutocompleteListItem{
|
||||
{Item: "item1", Hint: "this is hint 1", HelpText: "This is help text 1."},
|
||||
|
||||
@@ -282,15 +282,23 @@ func (th *TestHelper) CreateBot() *model.Bot {
|
||||
return bot
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateChannel(team *model.Team) *model.Channel {
|
||||
return th.createChannel(team, model.CHANNEL_OPEN)
|
||||
type ChannelOption func(*model.Channel)
|
||||
|
||||
func WithShared(v bool) ChannelOption {
|
||||
return func(channel *model.Channel) {
|
||||
channel.Shared = model.NewBool(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateChannel(team *model.Team, options ...ChannelOption) *model.Channel {
|
||||
return th.createChannel(team, model.CHANNEL_OPEN, options...)
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreatePrivateChannel(team *model.Team) *model.Channel {
|
||||
return th.createChannel(team, model.CHANNEL_PRIVATE)
|
||||
}
|
||||
|
||||
func (th *TestHelper) createChannel(team *model.Team, channelType string) *model.Channel {
|
||||
func (th *TestHelper) createChannel(team *model.Team, channelType string, options ...ChannelOption) *model.Channel {
|
||||
id := model.NewId()
|
||||
|
||||
channel := &model.Channel{
|
||||
@@ -301,10 +309,31 @@ func (th *TestHelper) createChannel(team *model.Team, channelType string) *model
|
||||
CreatorId: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(channel)
|
||||
}
|
||||
|
||||
utils.DisableDebugLogForTest()
|
||||
var err *model.AppError
|
||||
if channel, err = th.App.CreateChannel(channel, true); err != nil {
|
||||
panic(err)
|
||||
var appErr *model.AppError
|
||||
if channel, appErr = th.App.CreateChannel(channel, true); appErr != nil {
|
||||
panic(appErr)
|
||||
}
|
||||
|
||||
if channel.IsShared() {
|
||||
id := model.NewId()
|
||||
_, err := th.App.SaveSharedChannel(&model.SharedChannel{
|
||||
ChannelId: channel.Id,
|
||||
TeamId: channel.TeamId,
|
||||
Home: false,
|
||||
ReadOnly: false,
|
||||
ShareName: "shared-" + id,
|
||||
ShareDisplayName: "shared-" + id,
|
||||
CreatorId: th.BasicUser.Id,
|
||||
RemoteId: model.NewId(),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
utils.EnableDebugLogForTest()
|
||||
return channel
|
||||
|
||||
@@ -72,7 +72,7 @@ func (a *App) DoPostActionWithCookie(postID, actionId, userID, selectedOption st
|
||||
// Start all queries here for parallel execution
|
||||
pchan := make(chan store.StoreResult, 1)
|
||||
go func() {
|
||||
post, err := a.Srv().Store.Post().GetSingle(postID)
|
||||
post, err := a.Srv().Store.Post().GetSingle(postID, false)
|
||||
pchan <- store.StoreResult{Data: post, NErr: err}
|
||||
close(pchan)
|
||||
}()
|
||||
|
||||
@@ -419,7 +419,7 @@ func TestPostActionProps(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
assert.True(t, len(clientTriggerId) == 26)
|
||||
|
||||
newPost, nErr := th.App.Srv().Store.Post().GetSingle(post.Id)
|
||||
newPost, nErr := th.App.Srv().Store.Post().GetSingle(post.Id, false)
|
||||
require.NoError(t, nErr)
|
||||
|
||||
assert.True(t, newPost.IsPinned)
|
||||
|
||||
@@ -423,6 +423,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
|
||||
}
|
||||
|
||||
a.Publish(message)
|
||||
|
||||
// If this is a reply in a thread, notify participants
|
||||
if a.Config().FeatureFlags.CollapsedThreads && *a.Config().ServiceSettings.CollapsedThreads != model.COLLAPSED_THREADS_DISABLED && post.RootId != "" {
|
||||
thread, err := a.Srv().Store.Thread().Get(post.RootId)
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v5/plugin"
|
||||
"github.com/mattermost/mattermost-server/v5/services/httpservice"
|
||||
"github.com/mattermost/mattermost-server/v5/services/imageproxy"
|
||||
"github.com/mattermost/mattermost-server/v5/services/remotecluster"
|
||||
"github.com/mattermost/mattermost-server/v5/services/searchengine"
|
||||
"github.com/mattermost/mattermost-server/v5/services/timezones"
|
||||
"github.com/mattermost/mattermost-server/v5/services/tracing"
|
||||
@@ -235,6 +236,28 @@ func (a *OpenTracingAppLayer) AddPublicKey(name string, key io.Reader) *model.Ap
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) AddRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddRemoteCluster")
|
||||
|
||||
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.AddRemoteCluster(rc)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) AddSamlIdpCertificate(fileData *multipart.FileHeader) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddSamlIdpCertificate")
|
||||
@@ -1053,6 +1076,28 @@ func (a *OpenTracingAppLayer) CheckAndSendUserLimitWarningEmails() *model.AppErr
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) CheckCanInviteToSharedChannel(channelId string) error {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckCanInviteToSharedChannel")
|
||||
|
||||
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.CheckCanInviteToSharedChannel(channelId)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) CheckForClientSideCert(r *http.Request) (string, string, string) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckForClientSideCert")
|
||||
@@ -3088,6 +3133,28 @@ func (a *OpenTracingAppLayer) DeleteReactionForPost(reaction *model.Reaction) *m
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DeleteRemoteCluster(remoteClusterId string) (bool, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteRemoteCluster")
|
||||
|
||||
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.DeleteRemoteCluster(remoteClusterId)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DeleteScheme(schemeId string) (*model.Scheme, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteScheme")
|
||||
@@ -3110,6 +3177,50 @@ 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")
|
||||
|
||||
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.DeleteSharedChannelRemote(id)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DeleteSidebarCategory(userID string, teamID string, categoryId string) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteSidebarCategory")
|
||||
@@ -4240,6 +4351,28 @@ func (a *OpenTracingAppLayer) GetAllPublicTeamsPageWithCount(offset int, limit i
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetAllRemoteClusters(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllRemoteClusters")
|
||||
|
||||
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.GetAllRemoteClusters(filter)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetAllRoles() ([]*model.Role, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllRoles")
|
||||
@@ -6742,7 +6875,7 @@ func (a *OpenTracingAppLayer) GetOpenGraphMetadata(requestURL string) *opengraph
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetOrCreateDirectChannel(userID string, otherUserID string) (*model.Channel, *model.AppError) {
|
||||
func (a *OpenTracingAppLayer) GetOrCreateDirectChannel(userID string, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOrCreateDirectChannel")
|
||||
|
||||
@@ -6754,7 +6887,7 @@ func (a *OpenTracingAppLayer) GetOrCreateDirectChannel(userID string, otherUserI
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.GetOrCreateDirectChannel(userID, otherUserID)
|
||||
resultVar0, resultVar1 := a.app.GetOrCreateDirectChannel(userID, otherUserID, channelOptions...)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
@@ -7629,6 +7762,94 @@ func (a *OpenTracingAppLayer) GetRecentlyActiveUsersForTeamPage(teamID string, p
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetRemoteCluster(remoteClusterId string) (*model.RemoteCluster, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRemoteCluster")
|
||||
|
||||
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.GetRemoteCluster(remoteClusterId)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetRemoteClusterForUser(remoteID string, userID string) (*model.RemoteCluster, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRemoteClusterForUser")
|
||||
|
||||
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.GetRemoteClusterForUser(remoteID, userID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetRemoteClusterService() (remotecluster.RemoteClusterServiceIFace, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRemoteClusterService")
|
||||
|
||||
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.GetRemoteClusterService()
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetRemoteClusterSession(token string, remoteId string) (*model.Session, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRemoteClusterSession")
|
||||
|
||||
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.GetRemoteClusterSession(token, remoteId)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetRole(id string) (*model.Role, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRole")
|
||||
@@ -8005,6 +8226,160 @@ func (a *OpenTracingAppLayer) GetSessions(userID string) ([]*model.Session, *mod
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetSharedChannel(channelID string) (*model.SharedChannel, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannel")
|
||||
|
||||
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.GetSharedChannel(channelID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetSharedChannelRemote(id string) (*model.SharedChannelRemote, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelRemote")
|
||||
|
||||
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.GetSharedChannelRemote(id)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetSharedChannelRemoteByIds(channelID string, remoteID string) (*model.SharedChannelRemote, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelRemoteByIds")
|
||||
|
||||
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.GetSharedChannelRemoteByIds(channelID, remoteID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetSharedChannelRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelRemotes")
|
||||
|
||||
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.GetSharedChannelRemotes(opts)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetSharedChannelRemotesStatus(channelID string) ([]*model.SharedChannelRemoteStatus, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelRemotesStatus")
|
||||
|
||||
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.GetSharedChannelRemotesStatus(channelID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetSharedChannels(page int, perPage int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannels")
|
||||
|
||||
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.GetSharedChannels(page, perPage, opts)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetSharedChannelsCount(opts model.SharedChannelFilterOpts) (int64, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelsCount")
|
||||
|
||||
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.GetSharedChannelsCount(opts)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetSidebarCategories(userID string, teamID string) (*model.OrderedSidebarCategories, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSidebarCategories")
|
||||
@@ -9711,6 +10086,50 @@ func (a *OpenTracingAppLayer) HasPermissionToUser(askingUserId string, userID st
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) HasRemote(channelID string, remoteID string) (bool, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasRemote")
|
||||
|
||||
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.HasRemote(channelID, remoteID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) HasSharedChannel(channelID string) (bool, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasSharedChannel")
|
||||
|
||||
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.HasSharedChannel(channelID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) HubRegister(webConn *app.WebConn) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HubRegister")
|
||||
@@ -12710,6 +13129,50 @@ func (a *OpenTracingAppLayer) SaveReactionForPost(reaction *model.Reaction) (*mo
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SaveSharedChannel(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(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")
|
||||
|
||||
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.SaveSharedChannelRemote(remote)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SaveUserTermsOfService(userID string, termsOfServiceId string, accepted bool) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveUserTermsOfService")
|
||||
@@ -13974,6 +14437,28 @@ func (a *OpenTracingAppLayer) SetProfileImageFromMultiPartFile(userID string, fi
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SetRemoteClusterLastPingAt(remoteClusterId string) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetRemoteClusterLastPingAt")
|
||||
|
||||
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.SetRemoteClusterLastPingAt(remoteClusterId)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SetSamlIdpCertificateFromMetadata(data []byte) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetSamlIdpCertificateFromMetadata")
|
||||
@@ -15380,6 +15865,50 @@ func (a *OpenTracingAppLayer) UpdateProductNotices() *model.AppError {
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateRemoteCluster")
|
||||
|
||||
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.UpdateRemoteCluster(rc)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateRemoteClusterTopics(remoteClusterId string, topics string) (*model.RemoteCluster, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateRemoteClusterTopics")
|
||||
|
||||
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.UpdateRemoteClusterTopics(remoteClusterId, topics)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateRole(role *model.Role) (*model.Role, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateRole")
|
||||
@@ -15439,6 +15968,50 @@ func (a *OpenTracingAppLayer) UpdateSessionsIsGuest(userID string, isGuest bool)
|
||||
a.app.UpdateSessionsIsGuest(userID, isGuest)
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSharedChannel")
|
||||
|
||||
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.UpdateSharedChannel(sc)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateSharedChannelRemoteNextSyncAt(id string, syncTime int64) error {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSharedChannelRemoteNextSyncAt")
|
||||
|
||||
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.UpdateSharedChannelRemoteNextSyncAt(id, syncTime)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSidebarCategories")
|
||||
|
||||
@@ -181,7 +181,7 @@ func TestHookMessageWillBePosted(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, "message", post.Message)
|
||||
retrievedPost, errSingle := th.App.Srv().Store.Post().GetSingle(post.Id)
|
||||
retrievedPost, errSingle := th.App.Srv().Store.Post().GetSingle(post.Id, false)
|
||||
require.NoError(t, errSingle)
|
||||
assert.Equal(t, "message", retrievedPost.Message)
|
||||
})
|
||||
@@ -225,7 +225,7 @@ func TestHookMessageWillBePosted(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, "message_fromplugin", post.Message)
|
||||
retrievedPost, errSingle := th.App.Srv().Store.Post().GetSingle(post.Id)
|
||||
retrievedPost, errSingle := th.App.Srv().Store.Post().GetSingle(post.Id, false)
|
||||
require.NoError(t, errSingle)
|
||||
assert.Equal(t, "message_fromplugin", retrievedPost.Message)
|
||||
})
|
||||
|
||||
10
app/post.go
10
app/post.go
@@ -610,6 +610,10 @@ func (a *App) UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if post.IsRemote() {
|
||||
oldPost.RemoteId = model.NewString(*post.RemoteId)
|
||||
}
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
var rejectionReason string
|
||||
pluginContext := a.PluginContext()
|
||||
@@ -728,7 +732,7 @@ func (a *App) GetPostsSince(options model.GetPostsSinceOptions) (*model.PostList
|
||||
}
|
||||
|
||||
func (a *App) GetSinglePost(postID string) (*model.Post, *model.AppError) {
|
||||
post, err := a.Srv().Store.Post().GetSingle(postID)
|
||||
post, err := a.Srv().Store.Post().GetSingle(postID, false)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
@@ -1012,7 +1016,7 @@ func (a *App) GetPostsForChannelAroundLastUnread(channelID, userID string, limit
|
||||
}
|
||||
|
||||
func (a *App) DeletePost(postID, deleteByID string) (*model.Post, *model.AppError) {
|
||||
post, nErr := a.Srv().Store.Post().GetSingle(postID)
|
||||
post, nErr := a.Srv().Store.Post().GetSingle(postID, false)
|
||||
if nErr != nil {
|
||||
return nil, model.NewAppError("DeletePost", "app.post.get.app_error", nil, nErr.Error(), http.StatusBadRequest)
|
||||
}
|
||||
@@ -1237,7 +1241,7 @@ func (a *App) GetFileInfosForPostWithMigration(postID string) ([]*model.FileInfo
|
||||
|
||||
pchan := make(chan store.StoreResult, 1)
|
||||
go func() {
|
||||
post, err := a.Srv().Store.Post().GetSingle(postID)
|
||||
post, err := a.Srv().Store.Post().GetSingle(postID, false)
|
||||
pchan <- store.StoreResult{Data: post, NErr: err}
|
||||
close(pchan)
|
||||
}()
|
||||
|
||||
@@ -2053,3 +2053,87 @@ func TestReplyToPostWithLag(t *testing.T) {
|
||||
require.NotNil(t, reply)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSharedChannelSyncForPostActions(t *testing.T) {
|
||||
t.Run("creating a post in a shared channel performs a content sync when sync service is running on that node", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
remoteClusterService := NewMockSharedChannelService(nil)
|
||||
th.App.srv.sharedChannelService = remoteClusterService
|
||||
testCluster := &testlib.FakeClusterInterface{}
|
||||
th.Server.Cluster = testCluster
|
||||
|
||||
user := th.BasicUser
|
||||
|
||||
channel := th.CreateChannel(th.BasicTeam, WithShared(true))
|
||||
|
||||
_, err := th.App.CreatePost(&model.Post{
|
||||
UserId: user.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "Hello folks",
|
||||
}, channel, false, true)
|
||||
require.Nil(t, err, "Creating a post should not error")
|
||||
|
||||
assert.Len(t, remoteClusterService.notifications, 1)
|
||||
assert.Equal(t, channel.Id, remoteClusterService.notifications[0])
|
||||
})
|
||||
|
||||
t.Run("updating a post in a shared channel performs a content sync when sync service is running on that node", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
remoteClusterService := NewMockSharedChannelService(nil)
|
||||
th.App.srv.sharedChannelService = remoteClusterService
|
||||
testCluster := &testlib.FakeClusterInterface{}
|
||||
th.Server.Cluster = testCluster
|
||||
|
||||
user := th.BasicUser
|
||||
|
||||
channel := th.CreateChannel(th.BasicTeam, WithShared(true))
|
||||
|
||||
post, err := th.App.CreatePost(&model.Post{
|
||||
UserId: user.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "Hello folks",
|
||||
}, channel, false, true)
|
||||
require.Nil(t, err, "Creating a post should not error")
|
||||
|
||||
_, err = th.App.UpdatePost(post, true)
|
||||
require.Nil(t, err, "Updating a post should not error")
|
||||
|
||||
assert.Len(t, remoteClusterService.notifications, 2)
|
||||
assert.Equal(t, channel.Id, remoteClusterService.notifications[0])
|
||||
assert.Equal(t, channel.Id, remoteClusterService.notifications[1])
|
||||
})
|
||||
|
||||
t.Run("deleting a post in a shared channel performs a content sync when sync service is running on that node", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
remoteClusterService := NewMockSharedChannelService(nil)
|
||||
th.App.srv.sharedChannelService = remoteClusterService
|
||||
testCluster := &testlib.FakeClusterInterface{}
|
||||
th.Server.Cluster = testCluster
|
||||
|
||||
user := th.BasicUser
|
||||
|
||||
channel := th.CreateChannel(th.BasicTeam, WithShared(true))
|
||||
|
||||
post, err := th.App.CreatePost(&model.Post{
|
||||
UserId: user.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "Hello folks",
|
||||
}, channel, false, true)
|
||||
require.Nil(t, err, "Creating a post should not error")
|
||||
|
||||
_, err = th.App.DeletePost(post.Id, user.Id)
|
||||
require.Nil(t, err, "Deleting a post should not error")
|
||||
|
||||
// one creation and two deletes
|
||||
assert.Len(t, remoteClusterService.notifications, 3)
|
||||
assert.Equal(t, channel.Id, remoteClusterService.notifications[0])
|
||||
assert.Equal(t, channel.Id, remoteClusterService.notifications[1])
|
||||
assert.Equal(t, channel.Id, remoteClusterService.notifications[2])
|
||||
})
|
||||
}
|
||||
|
||||
86
app/reaction_test.go
Обычный файл
86
app/reaction_test.go
Обычный файл
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/testlib"
|
||||
)
|
||||
|
||||
func TestSharedChannelSyncForReactionActions(t *testing.T) {
|
||||
t.Run("adding a reaction in a shared channel performs a content sync when sync service is running on that node", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
|
||||
sharedChannelService := NewMockSharedChannelService(nil)
|
||||
th.App.srv.sharedChannelService = sharedChannelService
|
||||
testCluster := &testlib.FakeClusterInterface{}
|
||||
th.Server.Cluster = testCluster
|
||||
|
||||
user := th.BasicUser
|
||||
|
||||
channel := th.CreateChannel(th.BasicTeam, WithShared(true))
|
||||
|
||||
post, err := th.App.CreatePost(&model.Post{
|
||||
UserId: user.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "Hello folks",
|
||||
}, channel, false, true)
|
||||
require.Nil(t, err, "Creating a post should not error")
|
||||
|
||||
reaction := &model.Reaction{
|
||||
UserId: user.Id,
|
||||
PostId: post.Id,
|
||||
EmojiName: "+1",
|
||||
}
|
||||
|
||||
_, err = th.App.SaveReactionForPost(reaction)
|
||||
require.Nil(t, err, "Adding a reaction should not error")
|
||||
|
||||
th.TearDown() // We need to enforce teardown because reaction instrumentation happens in a goroutine
|
||||
|
||||
assert.Len(t, sharedChannelService.notifications, 2)
|
||||
assert.Equal(t, channel.Id, sharedChannelService.notifications[0])
|
||||
assert.Equal(t, channel.Id, sharedChannelService.notifications[1])
|
||||
})
|
||||
|
||||
t.Run("removing a reaction in a shared channel performs a content sync when sync service is running on that node", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
|
||||
sharedChannelService := NewMockSharedChannelService(nil)
|
||||
th.App.srv.sharedChannelService = sharedChannelService
|
||||
testCluster := &testlib.FakeClusterInterface{}
|
||||
th.Server.Cluster = testCluster
|
||||
|
||||
user := th.BasicUser
|
||||
|
||||
channel := th.CreateChannel(th.BasicTeam, WithShared(true))
|
||||
|
||||
post, err := th.App.CreatePost(&model.Post{
|
||||
UserId: user.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "Hello folks",
|
||||
}, channel, false, true)
|
||||
require.Nil(t, err, "Creating a post should not error")
|
||||
|
||||
reaction := &model.Reaction{
|
||||
UserId: user.Id,
|
||||
PostId: post.Id,
|
||||
EmojiName: "+1",
|
||||
}
|
||||
|
||||
err = th.App.DeleteReactionForPost(reaction)
|
||||
require.Nil(t, err, "Adding a reaction should not error")
|
||||
|
||||
th.TearDown() // We need to enforce teardown because reaction instrumentation happens in a goroutine
|
||||
|
||||
assert.Len(t, sharedChannelService.notifications, 2)
|
||||
assert.Equal(t, channel.Id, sharedChannelService.notifications[0])
|
||||
assert.Equal(t, channel.Id, sharedChannelService.notifications[1])
|
||||
})
|
||||
}
|
||||
87
app/remote_cluster.go
Обычный файл
87
app/remote_cluster.go
Обычный файл
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/services/remotecluster"
|
||||
"github.com/mattermost/mattermost-server/v5/store/sqlstore"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
)
|
||||
|
||||
func (a *App) AddRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) {
|
||||
rc, err := a.Srv().Store.RemoteCluster().Save(rc)
|
||||
if err != nil {
|
||||
if sqlstore.IsUniqueConstraintError(errors.Cause(err), []string{sqlstore.RemoteClusterSiteURLUniqueIndex}) {
|
||||
return nil, model.NewAppError("AddRemoteCluster", "api.remote_cluster.save_not_unique.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return nil, model.NewAppError("AddRemoteCluster", "api.remote_cluster.save.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) {
|
||||
rc, err := a.Srv().Store.RemoteCluster().Update(rc)
|
||||
if err != nil {
|
||||
if sqlstore.IsUniqueConstraintError(errors.Cause(err), []string{sqlstore.RemoteClusterSiteURLUniqueIndex}) {
|
||||
return nil, model.NewAppError("UpdateRemoteCluster", "api.remote_cluster.update_not_unique.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return nil, model.NewAppError("UpdateRemoteCluster", "api.remote_cluster.update.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteRemoteCluster(remoteClusterId string) (bool, *model.AppError) {
|
||||
deleted, err := a.Srv().Store.RemoteCluster().Delete(remoteClusterId)
|
||||
if err != nil {
|
||||
return false, model.NewAppError("DeleteRemoteCluster", "api.remote_cluster.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
func (a *App) GetRemoteCluster(remoteClusterId string) (*model.RemoteCluster, *model.AppError) {
|
||||
rc, err := a.Srv().Store.RemoteCluster().Get(remoteClusterId)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetRemoteCluster", "api.remote_cluster.get.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (a *App) GetAllRemoteClusters(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, *model.AppError) {
|
||||
list, err := a.Srv().Store.RemoteCluster().GetAll(filter)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetAllRemoteClusters", "api.remote_cluster.get.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateRemoteClusterTopics(remoteClusterId string, topics string) (*model.RemoteCluster, *model.AppError) {
|
||||
rc, err := a.Srv().Store.RemoteCluster().UpdateTopics(remoteClusterId, topics)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("UpdateRemoteClusterTopics", "api.remote_cluster.save.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (a *App) SetRemoteClusterLastPingAt(remoteClusterId string) *model.AppError {
|
||||
err := a.Srv().Store.RemoteCluster().SetLastPingAt(remoteClusterId)
|
||||
if err != nil {
|
||||
return model.NewAppError("SetRemoteClusterLastPingAt", "api.remote_cluster.save.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) GetRemoteClusterService() (remotecluster.RemoteClusterServiceIFace, *model.AppError) {
|
||||
service := a.Srv().GetRemoteClusterService()
|
||||
if service == nil {
|
||||
return nil, model.NewAppError("GetRemoteClusterService", "api.remote_cluster.service_not_enabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
75
app/remote_cluster_service_mock.go
Обычный файл
75
app/remote_cluster_service_mock.go
Обычный файл
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/services/remotecluster"
|
||||
)
|
||||
|
||||
// MockOptionRemoteClusterService a mock of the remote cluster service
|
||||
type MockOptionRemoteClusterService func(service *mockRemoteClusterService)
|
||||
|
||||
func MockOptionRemoteClusterServiceWithActive(active bool) MockOptionRemoteClusterService {
|
||||
return func(mrcs *mockRemoteClusterService) {
|
||||
mrcs.active = active
|
||||
}
|
||||
}
|
||||
|
||||
func NewMockRemoteClusterService(service remotecluster.RemoteClusterServiceIFace, options ...MockOptionRemoteClusterService) *mockRemoteClusterService {
|
||||
mrcs := &mockRemoteClusterService{service, true}
|
||||
for _, option := range options {
|
||||
option(mrcs)
|
||||
}
|
||||
return mrcs
|
||||
}
|
||||
|
||||
type mockRemoteClusterService struct {
|
||||
remotecluster.RemoteClusterServiceIFace
|
||||
active bool
|
||||
}
|
||||
|
||||
func (mrcs *mockRemoteClusterService) Shutdown() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mrcs *mockRemoteClusterService) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mrcs *mockRemoteClusterService) Active() bool {
|
||||
return mrcs.active
|
||||
}
|
||||
|
||||
func (mrcs *mockRemoteClusterService) AddTopicListener(topic string, listener remotecluster.TopicListener) string {
|
||||
return model.NewId()
|
||||
}
|
||||
|
||||
func (mrcs *mockRemoteClusterService) RemoveTopicListener(listenerId string) {
|
||||
}
|
||||
|
||||
func (mrcs *mockRemoteClusterService) AddConnectionStateListener(listener remotecluster.ConnectionStateListener) string {
|
||||
return model.NewId()
|
||||
}
|
||||
|
||||
func (mrcs *mockRemoteClusterService) RemoveConnectionStateListener(listenerId string) {
|
||||
}
|
||||
|
||||
func (mrcs *mockRemoteClusterService) SendMsg(ctx context.Context, msg model.RemoteClusterMsg, rc *model.RemoteCluster, f remotecluster.SendMsgResultFunc) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mrcs *mockRemoteClusterService) SendFile(ctx context.Context, us *model.UploadSession, fi *model.FileInfo, rc *model.RemoteCluster, rp remotecluster.ReaderProvider, f remotecluster.SendFileResultFunc) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mrcs *mockRemoteClusterService) AcceptInvitation(invite *model.RemoteClusterInvite, name string, creatorId string, teamId string, siteURL string) (*model.RemoteCluster, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (mrcs *mockRemoteClusterService) ReceiveIncomingMsg(rc *model.RemoteCluster, msg model.RemoteClusterMsg) remotecluster.Response {
|
||||
return remotecluster.Response{}
|
||||
}
|
||||
152
app/remote_cluster_test.go
Обычный файл
152
app/remote_cluster_test.go
Обычный файл
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
)
|
||||
|
||||
func TestAddRemoteCluster(t *testing.T) {
|
||||
t.Run("adding remote cluster with duplicate site url and remote team id", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
remoteCluster := &model.RemoteCluster{
|
||||
RemoteTeamId: model.NewId(),
|
||||
DisplayName: "test",
|
||||
SiteURL: "http://localhost:8065",
|
||||
Token: "test",
|
||||
RemoteToken: "test",
|
||||
Topics: "",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
_, err := th.App.AddRemoteCluster(remoteCluster)
|
||||
require.Nil(t, err, "Adding a remote cluster should not error")
|
||||
|
||||
remoteCluster.RemoteId = model.NewId()
|
||||
_, err = th.App.AddRemoteCluster(remoteCluster)
|
||||
require.Error(t, err, "Adding a duplicate remote cluster should error")
|
||||
assert.Contains(t, err.Error(), "Remote cluster has already been added.")
|
||||
})
|
||||
|
||||
t.Run("adding remote cluster with duplicate site url or remote team id is allowed", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
remoteCluster := &model.RemoteCluster{
|
||||
RemoteTeamId: model.NewId(),
|
||||
DisplayName: "test",
|
||||
SiteURL: "http://localhost:8065",
|
||||
Token: "test",
|
||||
RemoteToken: "test",
|
||||
Topics: "",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
existingRemoteCluster, err := th.App.AddRemoteCluster(remoteCluster)
|
||||
require.Nil(t, err, "Adding a remote cluster should not error")
|
||||
|
||||
// Same site url but different remote team id
|
||||
remoteCluster.RemoteId = model.NewId()
|
||||
remoteCluster.RemoteTeamId = model.NewId()
|
||||
remoteCluster.SiteURL = existingRemoteCluster.SiteURL
|
||||
_, err = th.App.AddRemoteCluster(remoteCluster)
|
||||
assert.Nil(t, err, "Adding a remote cluster should not error")
|
||||
|
||||
// Same remote team id but different site url
|
||||
remoteCluster.RemoteId = model.NewId()
|
||||
remoteCluster.RemoteTeamId = existingRemoteCluster.RemoteTeamId
|
||||
remoteCluster.SiteURL = existingRemoteCluster.SiteURL + "/new"
|
||||
_, err = th.App.AddRemoteCluster(remoteCluster)
|
||||
assert.Nil(t, err, "Adding a remote cluster should not error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateRemoteCluster(t *testing.T) {
|
||||
t.Run("update remote cluster with an already existing site url and team id", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
remoteCluster := &model.RemoteCluster{
|
||||
RemoteTeamId: model.NewId(),
|
||||
DisplayName: "test",
|
||||
SiteURL: "http://localhost:8065",
|
||||
Token: "test",
|
||||
RemoteToken: "test",
|
||||
Topics: "",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
otherRemoteCluster := &model.RemoteCluster{
|
||||
RemoteTeamId: model.NewId(),
|
||||
DisplayName: "test",
|
||||
SiteURL: "http://localhost:8066",
|
||||
Token: "test",
|
||||
RemoteToken: "test",
|
||||
Topics: "",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
_, err := th.App.AddRemoteCluster(remoteCluster)
|
||||
require.Nil(t, err, "Adding a remote cluster should not error")
|
||||
|
||||
savedRemoteClustered, err := th.App.AddRemoteCluster(otherRemoteCluster)
|
||||
require.Nil(t, err, "Adding a remote cluster should not error")
|
||||
|
||||
savedRemoteClustered.SiteURL = remoteCluster.SiteURL
|
||||
savedRemoteClustered.RemoteTeamId = remoteCluster.RemoteTeamId
|
||||
_, err = th.App.UpdateRemoteCluster(savedRemoteClustered)
|
||||
require.Error(t, err, "Updating remote cluster with duplicate site url should error")
|
||||
assert.Contains(t, err.Error(), "Remote cluster with the same url already exists.")
|
||||
})
|
||||
|
||||
t.Run("update remote cluster with an already existing site url or team id, is allowed", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
remoteCluster := &model.RemoteCluster{
|
||||
RemoteTeamId: model.NewId(),
|
||||
DisplayName: "test",
|
||||
SiteURL: "http://localhost:8065",
|
||||
Token: "test",
|
||||
RemoteToken: "test",
|
||||
Topics: "",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
otherRemoteCluster := &model.RemoteCluster{
|
||||
RemoteTeamId: model.NewId(),
|
||||
DisplayName: "test",
|
||||
SiteURL: "http://localhost:8066",
|
||||
Token: "test",
|
||||
RemoteToken: "test",
|
||||
Topics: "",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
existingRemoteCluster, err := th.App.AddRemoteCluster(remoteCluster)
|
||||
require.Nil(t, err, "Adding a remote cluster should not error")
|
||||
|
||||
anotherExistingRemoteClustered, err := th.App.AddRemoteCluster(otherRemoteCluster)
|
||||
require.Nil(t, err, "Adding a remote cluster should not error")
|
||||
|
||||
// Same site url but different remote team id
|
||||
anotherExistingRemoteClustered.SiteURL = existingRemoteCluster.SiteURL
|
||||
anotherExistingRemoteClustered.RemoteTeamId = model.NewId()
|
||||
_, err = th.App.UpdateRemoteCluster(anotherExistingRemoteClustered)
|
||||
assert.Nil(t, err, "Updating remote cluster should not error")
|
||||
|
||||
// Same remote team id but different site url
|
||||
anotherExistingRemoteClustered.SiteURL = existingRemoteCluster.SiteURL + "/new"
|
||||
anotherExistingRemoteClustered.RemoteTeamId = existingRemoteCluster.RemoteTeamId
|
||||
_, err = th.App.UpdateRemoteCluster(anotherExistingRemoteClustered)
|
||||
assert.Nil(t, err, "Updating remote cluster should not error")
|
||||
})
|
||||
}
|
||||
113
app/server.go
113
app/server.go
@@ -47,8 +47,10 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v5/services/cache"
|
||||
"github.com/mattermost/mattermost-server/v5/services/httpservice"
|
||||
"github.com/mattermost/mattermost-server/v5/services/imageproxy"
|
||||
"github.com/mattermost/mattermost-server/v5/services/remotecluster"
|
||||
"github.com/mattermost/mattermost-server/v5/services/searchengine"
|
||||
"github.com/mattermost/mattermost-server/v5/services/searchengine/bleveengine"
|
||||
"github.com/mattermost/mattermost-server/v5/services/sharedchannel"
|
||||
"github.com/mattermost/mattermost-server/v5/services/telemetry"
|
||||
"github.com/mattermost/mattermost-server/v5/services/timezones"
|
||||
"github.com/mattermost/mattermost-server/v5/services/tracing"
|
||||
@@ -157,6 +159,9 @@ type Server struct {
|
||||
|
||||
telemetryService *telemetry.TelemetryService
|
||||
|
||||
remoteClusterService remotecluster.RemoteClusterServiceIFace
|
||||
sharedChannelService SharedChannelServiceIFace
|
||||
|
||||
phase2PermissionsMigrationComplete bool
|
||||
|
||||
HTTPService httpservice.HTTPService
|
||||
@@ -808,6 +813,64 @@ func (s *Server) removeUnlicensedLogTargets(license *model.License) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) startInterClusterServices(license *model.License, app *App) error {
|
||||
if license == nil {
|
||||
mlog.Debug("No license provided; Remote Cluster services disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remote Cluster service
|
||||
|
||||
// License check
|
||||
if !*license.Features.RemoteClusterService {
|
||||
mlog.Debug("License does not have Remote Cluster services enabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config check
|
||||
if !*s.Config().ExperimentalSettings.EnableRemoteClusterService {
|
||||
mlog.Debug("Remote Cluster Service disabled via config")
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
s.remoteClusterService, err = remotecluster.NewRemoteClusterService(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.remoteClusterService.Start(); err != nil {
|
||||
s.remoteClusterService = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Shared Channels service
|
||||
|
||||
// License check
|
||||
if !*license.Features.SharedChannels {
|
||||
mlog.Debug("License does not have shared channels enabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config check
|
||||
if !*s.Config().ExperimentalSettings.EnableSharedChannels {
|
||||
mlog.Debug("Shared Channels Service disabled via config")
|
||||
return nil
|
||||
}
|
||||
|
||||
s.sharedChannelService, err = sharedchannel.NewSharedChannelService(s, app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.sharedChannelService.Start(); err != nil {
|
||||
s.remoteClusterService = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) enableLoggingMetrics() {
|
||||
if s.Metrics == nil {
|
||||
return
|
||||
@@ -866,6 +929,12 @@ func (s *Server) Shutdown() {
|
||||
mlog.Warn("Unable to cleanly shutdown telemetry client", mlog.Err(err))
|
||||
}
|
||||
|
||||
if s.remoteClusterService != nil {
|
||||
if err = s.remoteClusterService.Shutdown(); err != nil {
|
||||
mlog.Error("Error shutting down intercluster services", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
s.StopHTTPServer()
|
||||
s.stopLocalModeServer()
|
||||
// Push notification hub needs to be shutdown after HTTP server
|
||||
@@ -1231,6 +1300,10 @@ func (s *Server) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.startInterClusterServices(s.License(), s.WebSocketRouter.app); err != nil {
|
||||
mlog.Error("Error starting inter-cluster services", mlog.Err(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1799,6 +1872,46 @@ func (s *Server) SetLog(l *mlog.Logger) {
|
||||
s.Log = l
|
||||
}
|
||||
|
||||
func (s *Server) GetLogger() mlog.LoggerIFace {
|
||||
return s.Log
|
||||
}
|
||||
|
||||
// GetStore returns the server's Store. Exposing via a method
|
||||
// allows interfaces to be created with subsets of server APIs.
|
||||
func (s *Server) GetStore() store.Store {
|
||||
return s.Store
|
||||
}
|
||||
|
||||
// GetRemoteClusterService returns the `RemoteClusterService` instantiated by the server.
|
||||
// May be nil if the service is not enabled via license.
|
||||
func (s *Server) GetRemoteClusterService() remotecluster.RemoteClusterServiceIFace {
|
||||
return s.remoteClusterService
|
||||
}
|
||||
|
||||
// GetSharedChannelSyncService returns the `SharedChannelSyncService` instantiated by the server.
|
||||
// May be nil if the service is not enabled via license.
|
||||
func (s *Server) GetSharedChannelSyncService() SharedChannelServiceIFace {
|
||||
return s.sharedChannelService
|
||||
}
|
||||
|
||||
// GetMetrics returns the server's Metrics interface. Exposing via a method
|
||||
// allows interfaces to be created with subsets of server APIs.
|
||||
func (s *Server) GetMetrics() einterfaces.MetricsInterface {
|
||||
return s.Metrics
|
||||
}
|
||||
|
||||
// SetRemoteClusterService sets the `RemoteClusterService` to be used by the server.
|
||||
// For testing only.
|
||||
func (s *Server) SetRemoteClusterService(remoteClusterService remotecluster.RemoteClusterServiceIFace) {
|
||||
s.remoteClusterService = remoteClusterService
|
||||
}
|
||||
|
||||
// SetSharedChannelSyncService sets the `SharedChannelSyncService` to be used by the server.
|
||||
// For testing only.
|
||||
func (s *Server) SetSharedChannelSyncService(sharedChannelService SharedChannelServiceIFace) {
|
||||
s.sharedChannelService = sharedChannelService
|
||||
}
|
||||
|
||||
func (a *App) GenerateSupportPacket() []model.FileData {
|
||||
// If any errors we come across within this function, we will log it in a warning.txt file so that we know why certain files did not get produced if any
|
||||
var warnings []string
|
||||
|
||||
@@ -67,6 +67,21 @@ func (a *App) GetCloudSession(token string) (*model.Session, *model.AppError) {
|
||||
return nil, model.NewAppError("GetCloudSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token, "Error": ""}, "The provided token is invalid", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func (a *App) GetRemoteClusterSession(token string, remoteId string) (*model.Session, *model.AppError) {
|
||||
rc, appErr := a.GetRemoteCluster(remoteId)
|
||||
if appErr == nil && rc.Token == token {
|
||||
// Need a bare-bones session object for later checks
|
||||
session := &model.Session{
|
||||
Token: token,
|
||||
IsOAuth: false,
|
||||
}
|
||||
|
||||
session.AddProp(model.SESSION_PROP_TYPE, model.SESSION_TYPE_REMOTECLUSTER_TOKEN)
|
||||
return session, nil
|
||||
}
|
||||
return nil, model.NewAppError("GetRemoteClusterSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token, "Error": ""}, "The provided token is invalid", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func (a *App) GetSession(token string) (*model.Session, *model.AppError) {
|
||||
metrics := a.Metrics()
|
||||
|
||||
|
||||
@@ -439,3 +439,39 @@ func TestGetCloudSession(t *testing.T) {
|
||||
require.Equal(t, "api.context.invalid_token.error", err.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetRemoteClusterSession(t *testing.T) {
|
||||
th := Setup(t)
|
||||
token := model.NewId()
|
||||
remoteId := model.NewId()
|
||||
|
||||
rc := model.RemoteCluster{
|
||||
RemoteId: remoteId,
|
||||
RemoteTeamId: model.NewId(),
|
||||
DisplayName: "test",
|
||||
Token: token,
|
||||
CreatorId: model.NewId(),
|
||||
}
|
||||
|
||||
_, err := th.GetSqlStore().RemoteCluster().Save(&rc)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("Valid remote token should return session", func(t *testing.T) {
|
||||
session, err := th.App.GetRemoteClusterSession(token, remoteId)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, session)
|
||||
require.Equal(t, token, session.Token)
|
||||
})
|
||||
|
||||
t.Run("Invalid remote token should return error", func(t *testing.T) {
|
||||
session, err := th.App.GetRemoteClusterSession(model.NewId(), remoteId)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, session)
|
||||
})
|
||||
|
||||
t.Run("Invalid remote id should return error", func(t *testing.T) {
|
||||
session, err := th.App.GetRemoteClusterSession(token, model.NewId())
|
||||
require.Error(t, err)
|
||||
require.Nil(t, session)
|
||||
})
|
||||
}
|
||||
|
||||
149
app/shared_channel.go
Обычный файл
149
app/shared_channel.go
Обычный файл
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/store"
|
||||
)
|
||||
|
||||
func (a *App) checkChannelNotShared(channelId string) error {
|
||||
// check that channel exists.
|
||||
if _, err := a.GetChannel(channelId); err != nil {
|
||||
return fmt.Errorf("cannot share this channel: %w", err)
|
||||
}
|
||||
|
||||
// Check channel is not already shared.
|
||||
if _, err := a.GetSharedChannel(channelId); err == nil {
|
||||
var errNotFound *store.ErrNotFound
|
||||
if errors.As(err, &errNotFound) {
|
||||
return errors.New("channel is already shared.")
|
||||
}
|
||||
return fmt.Errorf("cannot find channel: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) checkChannelIsShared(channelId string) error {
|
||||
if _, err := a.GetSharedChannel(channelId); err != nil {
|
||||
var errNotFound *store.ErrNotFound
|
||||
if errors.As(err, &errNotFound) {
|
||||
return errors.New("channel is not shared.")
|
||||
}
|
||||
return fmt.Errorf("cannot find channel: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) CheckCanInviteToSharedChannel(channelId string) error {
|
||||
sc, err := a.GetSharedChannel(channelId)
|
||||
if err != nil {
|
||||
var errNotFound *store.ErrNotFound
|
||||
if errors.As(err, &errNotFound) {
|
||||
return errors.New("channel is not shared.")
|
||||
}
|
||||
return fmt.Errorf("cannot find channel: %w", err)
|
||||
}
|
||||
|
||||
if !sc.Home {
|
||||
return errors.New("channel is homed on a remote cluster.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SharedChannels
|
||||
|
||||
func (a *App) SaveSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error) {
|
||||
if err := a.checkChannelNotShared(sc.ChannelId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.Srv().Store.SharedChannel().Save(sc)
|
||||
}
|
||||
|
||||
func (a *App) GetSharedChannel(channelID string) (*model.SharedChannel, error) {
|
||||
return a.Srv().Store.SharedChannel().Get(channelID)
|
||||
}
|
||||
|
||||
func (a *App) HasSharedChannel(channelID string) (bool, error) {
|
||||
return a.Srv().Store.SharedChannel().HasChannel(channelID)
|
||||
}
|
||||
|
||||
func (a *App) GetSharedChannels(page int, perPage int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, *model.AppError) {
|
||||
channels, err := a.Srv().Store.SharedChannel().GetAll(page*perPage, perPage, opts)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetSharedChannels", "app.channel.get_channels.not_found.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (a *App) GetSharedChannelsCount(opts model.SharedChannelFilterOpts) (int64, error) {
|
||||
return a.Srv().Store.SharedChannel().GetAllCount(opts)
|
||||
}
|
||||
|
||||
func (a *App) UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error) {
|
||||
return a.Srv().Store.SharedChannel().Update(sc)
|
||||
}
|
||||
|
||||
func (a *App) DeleteSharedChannel(channelID string) (bool, error) {
|
||||
return a.Srv().Store.SharedChannel().Delete(channelID)
|
||||
}
|
||||
|
||||
// SharedChannelRemotes
|
||||
|
||||
func (a *App) SaveSharedChannelRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
|
||||
if err := a.checkChannelIsShared(remote.ChannelId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.Srv().Store.SharedChannel().SaveRemote(remote)
|
||||
}
|
||||
|
||||
func (a *App) GetSharedChannelRemote(id string) (*model.SharedChannelRemote, error) {
|
||||
return a.Srv().Store.SharedChannel().GetRemote(id)
|
||||
}
|
||||
|
||||
func (a *App) GetSharedChannelRemoteByIds(channelID string, remoteID string) (*model.SharedChannelRemote, error) {
|
||||
return a.Srv().Store.SharedChannel().GetRemoteByIds(channelID, remoteID)
|
||||
}
|
||||
|
||||
func (a *App) GetSharedChannelRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
|
||||
return a.Srv().Store.SharedChannel().GetRemotes(opts)
|
||||
}
|
||||
|
||||
// HasRemote returns whether a given channelID is present in the channel remotes or not.
|
||||
func (a *App) HasRemote(channelID string, remoteID string) (bool, error) {
|
||||
return a.Srv().Store.SharedChannel().HasRemote(channelID, remoteID)
|
||||
}
|
||||
|
||||
func (a *App) GetRemoteClusterForUser(remoteID string, userID string) (*model.RemoteCluster, *model.AppError) {
|
||||
rc, err := a.Srv().Store.SharedChannel().GetRemoteForUser(remoteID, userID)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.NewAppError("GetRemoteClusterForUser", "api.context.remote_id_invalid.app_error", nil, nfErr.Error(), http.StatusNotFound)
|
||||
default:
|
||||
return nil, model.NewAppError("GetRemoteClusterForUser", "api.context.remote_id_invalid.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateSharedChannelRemoteNextSyncAt(id string, syncTime int64) error {
|
||||
return a.Srv().Store.SharedChannel().UpdateRemoteNextSyncAt(id, syncTime)
|
||||
}
|
||||
|
||||
func (a *App) DeleteSharedChannelRemote(id string) (bool, error) {
|
||||
return a.Srv().Store.SharedChannel().DeleteRemote(id)
|
||||
}
|
||||
|
||||
func (a *App) GetSharedChannelRemotesStatus(channelID string) ([]*model.SharedChannelRemoteStatus, error) {
|
||||
if err := a.checkChannelIsShared(channelID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.Srv().Store.SharedChannel().GetRemotesStatus(channelID)
|
||||
}
|
||||
144
app/shared_channel_notifier.go
Обычный файл
144
app/shared_channel_notifier.go
Обычный файл
@@ -0,0 +1,144 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/services/sharedchannel"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
||||
)
|
||||
|
||||
var sharedChannelEventsForSync model.StringArray = []string{
|
||||
model.WEBSOCKET_EVENT_POSTED,
|
||||
model.WEBSOCKET_EVENT_POST_EDITED,
|
||||
model.WEBSOCKET_EVENT_POST_DELETED,
|
||||
model.WEBSOCKET_EVENT_REACTION_ADDED,
|
||||
model.WEBSOCKET_EVENT_REACTION_REMOVED,
|
||||
}
|
||||
|
||||
var sharedChannelEventsForInvitation model.StringArray = []string{
|
||||
model.WEBSOCKET_EVENT_DIRECT_ADDED,
|
||||
}
|
||||
|
||||
// SharedChannelSyncHandler is called when a websocket event is received by a cluster node.
|
||||
// Only on the leader node it will notify the sync service to perform necessary updates to the remote for the given
|
||||
// shared channel.
|
||||
func (s *Server) SharedChannelSyncHandler(event *model.WebSocketEvent) {
|
||||
syncService := s.GetSharedChannelSyncService()
|
||||
if isEligibleForEvents(syncService, event, sharedChannelEventsForSync) {
|
||||
err := handleContentSync(s, syncService, event)
|
||||
if err != nil {
|
||||
mlog.Warn(
|
||||
err.Error(),
|
||||
mlog.String("event", event.EventType()),
|
||||
mlog.String("action", "content_sync"),
|
||||
)
|
||||
}
|
||||
} else if isEligibleForEvents(syncService, event, sharedChannelEventsForInvitation) {
|
||||
err := handleInvitation(s, syncService, event)
|
||||
if err != nil {
|
||||
mlog.Warn(
|
||||
err.Error(),
|
||||
mlog.String("event", event.EventType()),
|
||||
mlog.String("action", "invitation"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isEligibleForEvents(syncService SharedChannelServiceIFace, event *model.WebSocketEvent, events model.StringArray) bool {
|
||||
return syncServiceEnabled(syncService) &&
|
||||
eventHasChannel(event) &&
|
||||
events.Contains(event.EventType())
|
||||
}
|
||||
|
||||
func eventHasChannel(event *model.WebSocketEvent) bool {
|
||||
return event.GetBroadcast() != nil &&
|
||||
event.GetBroadcast().ChannelId != ""
|
||||
}
|
||||
|
||||
func syncServiceEnabled(syncService SharedChannelServiceIFace) bool {
|
||||
return syncService != nil &&
|
||||
syncService.Active()
|
||||
}
|
||||
|
||||
func handleContentSync(s *Server, syncService SharedChannelServiceIFace, event *model.WebSocketEvent) error {
|
||||
channel, err := findChannel(s, event.GetBroadcast().ChannelId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if channel != nil && channel.IsShared() {
|
||||
syncService.NotifyChannelChanged(channel.Id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleInvitation(s *Server, syncService SharedChannelServiceIFace, event *model.WebSocketEvent) error {
|
||||
channel, err := findChannel(s, event.GetBroadcast().ChannelId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if channel == nil || !channel.IsShared() {
|
||||
return nil
|
||||
}
|
||||
|
||||
creator, err := getUserFromEvent(s, event, "creator_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// This is a termination condition, since on the other end when we are processing
|
||||
// the invite we are re-triggering a model.WEBSOCKET_EVENT_DIRECT_ADDED, which will call this handler.
|
||||
// When the creator is remote, it means that this is a DM that was not originated from the current server
|
||||
// and therefore we do not need to do anything.
|
||||
if creator == nil || creator.IsRemote() {
|
||||
return nil
|
||||
}
|
||||
|
||||
participant, err := getUserFromEvent(s, event, "teammate_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if participant == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rc, err := s.Store.RemoteCluster().Get(*participant.RemoteId)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("couldn't find remote cluster %s, for creating shared channel invitation for a DM", *participant.RemoteId))
|
||||
}
|
||||
|
||||
return syncService.SendChannelInvite(channel, creator.Id, "", rc, sharedchannel.WithDirectParticipantID(creator.Id), sharedchannel.WithDirectParticipantID(participant.Id))
|
||||
}
|
||||
|
||||
func getUserFromEvent(s *Server, event *model.WebSocketEvent, key string) (*model.User, error) {
|
||||
userID, ok := event.GetData()[key].(string)
|
||||
if !ok || userID == "" {
|
||||
return nil, fmt.Errorf("received websocket message that is eligible for sending an invitation but message does not have `%s` present", key)
|
||||
}
|
||||
|
||||
user, err := s.Store.User().Get(context.Background(), userID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "couldn't find user for creating shared channel invitation for a DM")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func findChannel(server *Server, channelId string) (*model.Channel, error) {
|
||||
channel, err := server.Store.Channel().Get(channelId, true)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "received websocket message that is eligible for shared channel sync but channel does not exist")
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
71
app/shared_channel_notifier_test.go
Обычный файл
71
app/shared_channel_notifier_test.go
Обычный файл
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
)
|
||||
|
||||
func TestServerSyncSharedChannelHandler(t *testing.T) {
|
||||
t.Run("sync service inactive, it does nothing", func(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
mockService := NewMockSharedChannelService(nil)
|
||||
mockService.active = false
|
||||
th.App.srv.sharedChannelService = mockService
|
||||
|
||||
th.App.srv.SharedChannelSyncHandler(&model.WebSocketEvent{})
|
||||
assert.Empty(t, mockService.notifications)
|
||||
})
|
||||
|
||||
t.Run("sync service active and broadcast envelope has ineligible event, it does nothing", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
mockService := NewMockSharedChannelService(nil)
|
||||
mockService.active = true
|
||||
th.App.srv.sharedChannelService = mockService
|
||||
channel := th.CreateChannel(th.BasicTeam, WithShared(true))
|
||||
|
||||
websocketEvent := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_ADDED_TO_TEAM, model.NewId(), channel.Id, "", nil)
|
||||
|
||||
th.App.srv.SharedChannelSyncHandler(websocketEvent)
|
||||
assert.Empty(t, mockService.notifications)
|
||||
})
|
||||
|
||||
t.Run("sync service active and broadcast envelope has eligible event but channel does not exist, it does nothing", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
mockService := NewMockSharedChannelService(nil)
|
||||
mockService.active = true
|
||||
th.App.srv.sharedChannelService = mockService
|
||||
|
||||
websocketEvent := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, model.NewId(), model.NewId(), "", nil)
|
||||
|
||||
th.App.srv.SharedChannelSyncHandler(websocketEvent)
|
||||
assert.Empty(t, mockService.notifications)
|
||||
})
|
||||
|
||||
t.Run("sync service active when received eligible event, it triggers a shared channel content sync", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
mockService := NewMockSharedChannelService(nil)
|
||||
mockService.active = true
|
||||
th.App.srv.sharedChannelService = mockService
|
||||
|
||||
channel := th.CreateChannel(th.BasicTeam, WithShared(true))
|
||||
websocketEvent := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, model.NewId(), channel.Id, "", nil)
|
||||
|
||||
th.App.srv.SharedChannelSyncHandler(websocketEvent)
|
||||
assert.Len(t, mockService.notifications, 1)
|
||||
assert.Equal(t, channel.Id, mockService.notifications[0])
|
||||
})
|
||||
}
|
||||
66
app/shared_channel_service_iface.go
Обычный файл
66
app/shared_channel_service_iface.go
Обычный файл
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/services/sharedchannel"
|
||||
)
|
||||
|
||||
// SharedChannelServiceIFace is the interface to the shared channel service
|
||||
type SharedChannelServiceIFace interface {
|
||||
Shutdown() error
|
||||
Start() error
|
||||
NotifyChannelChanged(channelId string)
|
||||
SendChannelInvite(channel *model.Channel, userId string, description string, rc *model.RemoteCluster, options ...sharedchannel.InviteOption) error
|
||||
Active() bool
|
||||
}
|
||||
|
||||
type MockOptionSharedChannelService func(service *mockSharedChannelService)
|
||||
|
||||
func MockOptionSharedChannelServiceWithActive(active bool) MockOptionSharedChannelService {
|
||||
return func(mrcs *mockSharedChannelService) {
|
||||
mrcs.active = active
|
||||
}
|
||||
}
|
||||
|
||||
func NewMockSharedChannelService(service SharedChannelServiceIFace, options ...MockOptionSharedChannelService) *mockSharedChannelService {
|
||||
mrcs := &mockSharedChannelService{service, true, []string{}, 0}
|
||||
for _, option := range options {
|
||||
option(mrcs)
|
||||
}
|
||||
return mrcs
|
||||
}
|
||||
|
||||
type mockSharedChannelService struct {
|
||||
SharedChannelServiceIFace
|
||||
active bool
|
||||
notifications []string
|
||||
numInvitations int
|
||||
}
|
||||
|
||||
func (mrcs *mockSharedChannelService) NotifyChannelChanged(channelId string) {
|
||||
mrcs.notifications = append(mrcs.notifications, channelId)
|
||||
}
|
||||
|
||||
func (mrcs *mockSharedChannelService) Shutdown() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mrcs *mockSharedChannelService) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mrcs *mockSharedChannelService) Active() bool {
|
||||
return mrcs.active
|
||||
}
|
||||
|
||||
func (mrcs *mockSharedChannelService) SendChannelInvite(channel *model.Channel, userId string, description string, rc *model.RemoteCluster, options ...sharedchannel.InviteOption) error {
|
||||
mrcs.numInvitations += 1
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mrcs *mockSharedChannelService) NumInvitations() int {
|
||||
return mrcs.numInvitations
|
||||
}
|
||||
89
app/shared_channel_test.go
Обычный файл
89
app/shared_channel_test.go
Обычный файл
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
)
|
||||
|
||||
func TestApp_CheckCanInviteToSharedChannel(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
|
||||
channel1 := th.CreateChannel(th.BasicTeam)
|
||||
channel2 := th.CreateChannel(th.BasicTeam)
|
||||
channel3 := th.CreateChannel(th.BasicTeam)
|
||||
|
||||
data := []struct {
|
||||
channelId string
|
||||
home bool
|
||||
name string
|
||||
remoteId string
|
||||
}{
|
||||
{channelId: channel1.Id, home: true, name: "test_home", remoteId: ""},
|
||||
{channelId: channel2.Id, home: false, name: "test_remote", remoteId: model.NewId()},
|
||||
}
|
||||
|
||||
for _, d := range data {
|
||||
sc := &model.SharedChannel{
|
||||
ChannelId: d.channelId,
|
||||
TeamId: th.BasicTeam.Id,
|
||||
Home: d.home,
|
||||
ShareName: d.name,
|
||||
CreatorId: th.BasicUser.Id,
|
||||
RemoteId: d.remoteId,
|
||||
}
|
||||
_, err := th.App.SaveSharedChannel(sc)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("Test checkChannelNotShared: not yet shared channel", func(t *testing.T) {
|
||||
err := th.App.checkChannelNotShared(channel3.Id)
|
||||
assert.NoError(t, err, "unshared channel should not error")
|
||||
})
|
||||
|
||||
t.Run("Test checkChannelNotShared: already shared channel", func(t *testing.T) {
|
||||
err := th.App.checkChannelNotShared(channel1.Id)
|
||||
assert.Error(t, err, "already shared channel should error")
|
||||
})
|
||||
|
||||
t.Run("Test checkChannelNotShared: invalid channel", func(t *testing.T) {
|
||||
err := th.App.checkChannelNotShared(model.NewId())
|
||||
assert.Error(t, err, "invalid channel should error")
|
||||
})
|
||||
|
||||
t.Run("Test checkChannelIsShared: not yet shared channel", func(t *testing.T) {
|
||||
err := th.App.checkChannelIsShared(channel3.Id)
|
||||
assert.Error(t, err, "unshared channel should error")
|
||||
})
|
||||
|
||||
t.Run("Test checkChannelIsShared: already shared channel", func(t *testing.T) {
|
||||
err := th.App.checkChannelIsShared(channel1.Id)
|
||||
assert.NoError(t, err, "already channel should not error")
|
||||
})
|
||||
|
||||
t.Run("Test checkChannelIsShared: invalid channel", func(t *testing.T) {
|
||||
err := th.App.checkChannelIsShared(model.NewId())
|
||||
assert.Error(t, err, "invalid channel should error")
|
||||
})
|
||||
|
||||
t.Run("Test CheckCanInviteToSharedChannel: Home shared channel", func(t *testing.T) {
|
||||
err := th.App.CheckCanInviteToSharedChannel(data[0].channelId)
|
||||
assert.NoError(t, err, "home channel should allow invites")
|
||||
})
|
||||
|
||||
t.Run("Test CheckCanInviteToSharedChannel: Remote shared channel", func(t *testing.T) {
|
||||
err := th.App.CheckCanInviteToSharedChannel(data[1].channelId)
|
||||
assert.Error(t, err, "home channel should not allow invites")
|
||||
})
|
||||
|
||||
t.Run("Test CheckCanInviteToSharedChannel: Invalid shared channel", func(t *testing.T) {
|
||||
err := th.App.CheckCanInviteToSharedChannel(model.NewId())
|
||||
assert.Error(t, err, "invalid channel should not allow invites")
|
||||
})
|
||||
}
|
||||
292
app/slashcommands/command_remote.go
Обычный файл
292
app/slashcommands/command_remote.go
Обычный файл
@@ -0,0 +1,292 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package slashcommands
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/app"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/i18n"
|
||||
)
|
||||
|
||||
const (
|
||||
AvailableRemoteActions = "invite, accept, remove, status"
|
||||
)
|
||||
|
||||
type RemoteProvider struct {
|
||||
}
|
||||
|
||||
const (
|
||||
CommandTriggerRemote = "remote"
|
||||
)
|
||||
|
||||
func init() {
|
||||
app.RegisterCommandProvider(&RemoteProvider{})
|
||||
}
|
||||
|
||||
func (rp *RemoteProvider) GetTrigger() string {
|
||||
return CommandTriggerRemote
|
||||
}
|
||||
|
||||
func (rp *RemoteProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
|
||||
|
||||
remote := model.NewAutocompleteData(rp.GetTrigger(), "[action]", T("api.command_remote.remote_add_remove.help", map[string]interface{}{"Actions": AvailableRemoteActions}))
|
||||
|
||||
invite := model.NewAutocompleteData("invite", "", T("api.command_remote.invite.help"))
|
||||
invite.AddNamedTextArgument("password", T("api.command_remote.invite_password.help"), T("api.command_remote.invite_password.hint"), "", true)
|
||||
invite.AddNamedTextArgument("name", T("api.command_remote.name.help"), T("api.command_remote.name.hint"), "", true)
|
||||
|
||||
accept := model.NewAutocompleteData("accept", "", T("api.command_remote.accept.help"))
|
||||
accept.AddNamedTextArgument("password", T("api.command_remote.invite_password.help"), T("api.command_remote.invite_password.hint"), "", true)
|
||||
accept.AddNamedTextArgument("name", T("api.command_remote.name.help"), T("api.command_remote.name.hint"), "", true)
|
||||
accept.AddNamedTextArgument("invite", T("api.command_remote.invitation.help"), T("api.command_remote.invitation.hint"), "", true)
|
||||
|
||||
remove := model.NewAutocompleteData("remove", "", T("api.command_remote.remove.help"))
|
||||
remove.AddNamedDynamicListArgument("remoteId", T("api.command_remote.remove_remote_id.help"), "builtin:remote", true)
|
||||
|
||||
status := model.NewAutocompleteData("status", "", T("api.command_remote.status.help"))
|
||||
|
||||
remote.AddCommand(invite)
|
||||
remote.AddCommand(accept)
|
||||
remote.AddCommand(remove)
|
||||
remote.AddCommand(status)
|
||||
|
||||
return &model.Command{
|
||||
Trigger: rp.GetTrigger(),
|
||||
AutoComplete: true,
|
||||
AutoCompleteDesc: T("api.command_remote.desc"),
|
||||
AutoCompleteHint: T("api.command_remote.hint"),
|
||||
DisplayName: T("api.command_remote.name"),
|
||||
AutocompleteData: remote,
|
||||
}
|
||||
}
|
||||
|
||||
func (rp *RemoteProvider) DoCommand(a *app.App, args *model.CommandArgs, message string) *model.CommandResponse {
|
||||
if !a.HasPermissionTo(args.UserId, model.PERMISSION_MANAGE_SHARED_CHANNELS) {
|
||||
return responsef(args.T("api.command_remote.permission_required", map[string]interface{}{"Permission": "manage_shared_channels"}))
|
||||
}
|
||||
|
||||
margs := parseNamedArgs(args.Command)
|
||||
action, ok := margs[ActionKey]
|
||||
if !ok {
|
||||
return responsef(args.T("api.command_remote.missing_command", map[string]interface{}{"Actions": AvailableRemoteActions}))
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "invite":
|
||||
return rp.doInvite(a, args, margs)
|
||||
case "accept":
|
||||
return rp.doAccept(a, args, margs)
|
||||
case "remove":
|
||||
return rp.doRemove(a, args, margs)
|
||||
case "status":
|
||||
return rp.doStatus(a, args, margs)
|
||||
}
|
||||
|
||||
return responsef(args.T("api.command_remote.unknown_action", map[string]interface{}{"Action": action}))
|
||||
}
|
||||
|
||||
func (rp *RemoteProvider) GetAutoCompleteListItems(a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
|
||||
if !a.HasPermissionTo(commandArgs.UserId, model.PERMISSION_MANAGE_SHARED_CHANNELS) {
|
||||
return nil, errors.New("You require `manage_shared_channels` permission to manage remote clusters.")
|
||||
}
|
||||
|
||||
if arg.Name == "remoteId" && strings.Contains(parsed, " remove ") {
|
||||
return getRemoteClusterAutocompleteListItems(a, true)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("`%s` is not a dynamic argument", arg.Name)
|
||||
}
|
||||
|
||||
// doInvite creates and displays an encrypted invite that can be used by a remote site to establish a simple trust.
|
||||
func (rp *RemoteProvider) doInvite(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
|
||||
password := margs["password"]
|
||||
if password == "" {
|
||||
return responsef(args.T("api.command_remote.missing_empty", map[string]interface{}{"Arg": "password"}))
|
||||
}
|
||||
|
||||
name := margs["name"]
|
||||
if name == "" {
|
||||
return responsef(args.T("api.command_remote.missing_empty", map[string]interface{}{"Arg": "name"}))
|
||||
}
|
||||
|
||||
url := a.GetSiteURL()
|
||||
if url == "" {
|
||||
return responsef(args.T("api.command_remote.site_url_not_set"))
|
||||
}
|
||||
|
||||
rc := &model.RemoteCluster{
|
||||
DisplayName: name,
|
||||
Token: model.NewId(),
|
||||
CreatorId: args.UserId,
|
||||
}
|
||||
|
||||
rcSaved, appErr := a.AddRemoteCluster(rc)
|
||||
if appErr != nil {
|
||||
return responsef(args.T("api.command_remote.add_remote.error", map[string]interface{}{"Error": appErr.Error()}))
|
||||
}
|
||||
|
||||
// Display the encrypted invitation
|
||||
invite := &model.RemoteClusterInvite{
|
||||
RemoteId: rcSaved.RemoteId,
|
||||
RemoteTeamId: args.TeamId,
|
||||
SiteURL: url,
|
||||
Token: rcSaved.Token,
|
||||
}
|
||||
encrypted, err := invite.Encrypt(password)
|
||||
if err != nil {
|
||||
return responsef(args.T("api.command_remote.encrypt_invitation.error", map[string]interface{}{"Error": err.Error()}))
|
||||
}
|
||||
encoded := base64.URLEncoding.EncodeToString(encrypted)
|
||||
|
||||
return responsef("##### " + args.T("api.command_remote.invitation_created") + "\n" +
|
||||
args.T("api.command_remote.invite_summary", map[string]interface{}{"Command": "/remote accept", "Invitation": encoded, "SiteURL": invite.SiteURL}))
|
||||
}
|
||||
|
||||
// doAccept accepts an invitation generated by a remote site.
|
||||
func (rp *RemoteProvider) doAccept(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
|
||||
password := margs["password"]
|
||||
if password == "" {
|
||||
return responsef(args.T("api.command_remote.missing_empty", map[string]interface{}{"Arg": "password"}))
|
||||
}
|
||||
|
||||
name := margs["name"]
|
||||
if name == "" {
|
||||
return responsef(args.T("api.command_remote.missing_empty", map[string]interface{}{"Arg": "name"}))
|
||||
}
|
||||
|
||||
blob := margs["invite"]
|
||||
if blob == "" {
|
||||
return responsef(args.T("api.command_remote.missing_empty", map[string]interface{}{"Arg": "invite"}))
|
||||
}
|
||||
|
||||
// invite is encoded as base64 and encrypted
|
||||
decoded, err := base64.URLEncoding.DecodeString(blob)
|
||||
if err != nil {
|
||||
return responsef(args.T("api.command_remote.decode_invitation.error", map[string]interface{}{"Error": err.Error()}))
|
||||
}
|
||||
invite := &model.RemoteClusterInvite{}
|
||||
err = invite.Decrypt(decoded, password)
|
||||
if err != nil {
|
||||
return responsef(args.T("api.command_remote.incorrect_password.error", map[string]interface{}{"Error": err.Error()}))
|
||||
}
|
||||
|
||||
rcs, _ := a.GetRemoteClusterService()
|
||||
if rcs == nil {
|
||||
return responsef(args.T("api.command_remote.service_not_enabled"))
|
||||
}
|
||||
|
||||
url := a.GetSiteURL()
|
||||
if url == "" {
|
||||
return responsef(args.T("api.command_remote.site_url_not_set"))
|
||||
}
|
||||
|
||||
rc, err := rcs.AcceptInvitation(invite, name, args.UserId, args.TeamId, url)
|
||||
if err != nil {
|
||||
return responsef(args.T("api.command_remote.accept_invitation.error", map[string]interface{}{"Error": err.Error()}))
|
||||
}
|
||||
|
||||
return responsef("##### " + args.T("api.command_remote.accept_invitation", map[string]interface{}{"SiteURL": rc.SiteURL}))
|
||||
}
|
||||
|
||||
// doRemove removes a remote cluster from the database, effectively revoking the trust relationship.
|
||||
func (rp *RemoteProvider) doRemove(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
|
||||
id, ok := margs["remoteId"]
|
||||
if !ok {
|
||||
return responsef(args.T("api.command_remote.missing_empty", map[string]interface{}{"Arg": "remoteId"}))
|
||||
}
|
||||
|
||||
deleted, err := a.DeleteRemoteCluster(id)
|
||||
if err != nil {
|
||||
responsef(args.T("api.command_remote.remove_remote.error", map[string]interface{}{"Error": err.Error()}))
|
||||
}
|
||||
|
||||
result := "removed"
|
||||
if !deleted {
|
||||
result = "**NOT FOUND**"
|
||||
}
|
||||
return responsef("##### " + args.T("api.command_remote.cluster_removed", map[string]interface{}{"RemoteId": id, "Result": result}))
|
||||
}
|
||||
|
||||
// doStatus displays connection status for all remote clusters.
|
||||
func (rp *RemoteProvider) doStatus(a *app.App, args *model.CommandArgs, _ map[string]string) *model.CommandResponse {
|
||||
list, err := a.GetAllRemoteClusters(model.RemoteClusterQueryFilter{})
|
||||
if err != nil {
|
||||
responsef(args.T("api.command_remote.fetch_status.error", map[string]interface{}{"Error": err.Error()}))
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return responsef("** " + args.T("api.command_remote.remotes_not_found") + " **")
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, args.T("api.command_remote.remote_table_header")+"| \n")
|
||||
fmt.Fprintf(&sb, "| ---- | -------- | ---------- | :-------------: | :----: | ---------- |\n")
|
||||
|
||||
for _, rc := range list {
|
||||
accepted := ":white_check_mark:"
|
||||
if rc.SiteURL == "" {
|
||||
accepted = ":x:"
|
||||
}
|
||||
|
||||
online := ":white_check_mark:"
|
||||
if !isOnline(rc.LastPingAt) {
|
||||
online = ":skull_and_crossbones:"
|
||||
}
|
||||
|
||||
lastPing := formatTimestamp(model.GetTimeForMillis(rc.LastPingAt))
|
||||
|
||||
fmt.Fprintf(&sb, "| %s | %s | %s | %s | %s | %s |\n", rc.DisplayName, rc.SiteURL, rc.RemoteId, accepted, online, lastPing)
|
||||
}
|
||||
return responsef(sb.String())
|
||||
}
|
||||
|
||||
func isOnline(lastPing int64) bool {
|
||||
return lastPing > model.GetMillis()-model.RemoteOfflineAfterMillis
|
||||
}
|
||||
|
||||
func getRemoteClusterAutocompleteListItems(a *app.App, includeOffline bool) ([]model.AutocompleteListItem, error) {
|
||||
filter := model.RemoteClusterQueryFilter{
|
||||
ExcludeOffline: !includeOffline,
|
||||
}
|
||||
clusters, err := a.GetAllRemoteClusters(filter)
|
||||
if err != nil || len(clusters) == 0 {
|
||||
return []model.AutocompleteListItem{}, nil
|
||||
}
|
||||
|
||||
list := make([]model.AutocompleteListItem, 0, len(clusters))
|
||||
|
||||
for _, rc := range clusters {
|
||||
item := model.AutocompleteListItem{
|
||||
Item: rc.RemoteId,
|
||||
HelpText: fmt.Sprintf("%s (%s)", rc.DisplayName, rc.SiteURL)}
|
||||
list = append(list, item)
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func getRemoteClusterAutocompleteListItemsNotInChannel(a *app.App, channelId string, includeOffline bool) ([]model.AutocompleteListItem, error) {
|
||||
filter := model.RemoteClusterQueryFilter{
|
||||
ExcludeOffline: !includeOffline,
|
||||
NotInChannel: channelId,
|
||||
}
|
||||
all, err := a.GetAllRemoteClusters(filter)
|
||||
if err != nil || len(all) == 0 {
|
||||
return []model.AutocompleteListItem{}, nil
|
||||
}
|
||||
|
||||
list := make([]model.AutocompleteListItem, 0, len(all))
|
||||
|
||||
for _, rc := range all {
|
||||
item := model.AutocompleteListItem{
|
||||
Item: rc.RemoteId,
|
||||
HelpText: fmt.Sprintf("%s (%s)", rc.DisplayName, rc.SiteURL)}
|
||||
list = append(list, item)
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
347
app/slashcommands/command_share.go
Обычный файл
347
app/slashcommands/command_share.go
Обычный файл
@@ -0,0 +1,347 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package slashcommands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/app"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/i18n"
|
||||
)
|
||||
|
||||
type ShareProvider struct {
|
||||
}
|
||||
|
||||
const (
|
||||
CommandTriggerShare = "share"
|
||||
AvailableShareActions = "share_channel, unshare_channel, invite_remove, uninvite_remote, status"
|
||||
)
|
||||
|
||||
func init() {
|
||||
app.RegisterCommandProvider(&ShareProvider{})
|
||||
}
|
||||
|
||||
func (sp *ShareProvider) GetTrigger() string {
|
||||
return CommandTriggerShare
|
||||
}
|
||||
|
||||
func (sp *ShareProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
|
||||
share := model.NewAutocompleteData(CommandTriggerShare, "[action]", T("api.command_share.available_actions", map[string]interface{}{"Actions": AvailableShareActions}))
|
||||
|
||||
shareChannel := model.NewAutocompleteData("share_channel", "", T("api.command_share.share_current"))
|
||||
shareChannel.AddNamedTextArgument("readonly", T("api.command_share.share_read_only.help"), T("api.command_share.share_read_only.hint"), "Y|N|y|n", false)
|
||||
shareChannel.AddNamedTextArgument("name", T("api.command_share.channel_name.help"), T("api.command_share.channel_name.hint"), "", false)
|
||||
shareChannel.AddNamedTextArgument("displayname", T("api.command_share.channel_display_name.help"), T("api.command_share.channel_display_name.hint"), "", false)
|
||||
shareChannel.AddNamedTextArgument("purpose", T("api.command_share.channel_purpose.help"), T("api.command_share.channel_purpose.hint"), "", false)
|
||||
shareChannel.AddNamedTextArgument("header", T("api.command_share.channel_header.help"), T("api.command_share.channel_header.hint"), "", false)
|
||||
|
||||
unshareChannel := model.NewAutocompleteData("unshare_channel", "", T("api.command_share.unshare_channel.help"))
|
||||
unshareChannel.AddNamedTextArgument("are_you_sure", T("api.command_share.unshare_confirmation.help"), T("api.command_share.unshare_confirmation.hint"), "Y|N|y|n", true)
|
||||
|
||||
inviteRemote := model.NewAutocompleteData("invite_remote", "", T("api.command_share.invite_remote.help"))
|
||||
inviteRemote.AddNamedDynamicListArgument("remoteId", T("api.command_share.remote_id.help"), "builtin:share", true)
|
||||
inviteRemote.AddNamedTextArgument("description", T("api.command_share.description_invite.help"), T("api.command_share.description_invite.hint"), "", false)
|
||||
|
||||
unInviteRemote := model.NewAutocompleteData("uninvite_remote", "", T("api.command_share.uninvite_remote.help"))
|
||||
unInviteRemote.AddNamedDynamicListArgument("remoteId", T("api.command_share.uninvite_remote_id.help"), "builtin:share", true)
|
||||
|
||||
status := model.NewAutocompleteData("status", "", T("api.command_share.channel_status.help"))
|
||||
|
||||
share.AddCommand(shareChannel)
|
||||
share.AddCommand(unshareChannel)
|
||||
share.AddCommand(inviteRemote)
|
||||
share.AddCommand(unInviteRemote)
|
||||
share.AddCommand(status)
|
||||
|
||||
return &model.Command{
|
||||
Trigger: CommandTriggerShare,
|
||||
AutoComplete: true,
|
||||
AutoCompleteDesc: T("api.command_share.desc"),
|
||||
AutoCompleteHint: T("api.command_share.hint"),
|
||||
DisplayName: T("api.command_share.name"),
|
||||
AutocompleteData: share,
|
||||
}
|
||||
}
|
||||
|
||||
func (sp *ShareProvider) GetAutoCompleteListItems(a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
|
||||
switch {
|
||||
case strings.Contains(parsed, " share_channel "):
|
||||
|
||||
return sp.getAutoCompleteShareChannel(a, commandArgs, arg)
|
||||
|
||||
case strings.Contains(parsed, " invite_remote "):
|
||||
|
||||
return sp.getAutoCompleteInviteRemote(a, commandArgs, arg)
|
||||
|
||||
case strings.Contains(parsed, " uninvite_remote "):
|
||||
|
||||
return sp.getAutoCompleteUnInviteRemote(a, commandArgs, arg)
|
||||
|
||||
}
|
||||
return nil, errors.New("invalid action")
|
||||
}
|
||||
|
||||
func (sp *ShareProvider) getAutoCompleteShareChannel(a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg) ([]model.AutocompleteListItem, error) {
|
||||
channel, err := a.GetChannel(commandArgs.ChannelId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var item model.AutocompleteListItem
|
||||
|
||||
switch arg.Name {
|
||||
case "name":
|
||||
item = model.AutocompleteListItem{
|
||||
Item: channel.Name,
|
||||
HelpText: channel.DisplayName,
|
||||
}
|
||||
case "displayname":
|
||||
item = model.AutocompleteListItem{
|
||||
Item: channel.DisplayName,
|
||||
HelpText: channel.Name,
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("%s not a dynamic argument", arg.Name)
|
||||
}
|
||||
return []model.AutocompleteListItem{item}, nil
|
||||
}
|
||||
|
||||
func (sp *ShareProvider) getAutoCompleteInviteRemote(a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg) ([]model.AutocompleteListItem, error) {
|
||||
switch arg.Name {
|
||||
case "remoteId":
|
||||
return getRemoteClusterAutocompleteListItemsNotInChannel(a, commandArgs.ChannelId, true)
|
||||
default:
|
||||
return nil, fmt.Errorf("%s not a dynamic argument", arg.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (sp *ShareProvider) getAutoCompleteUnInviteRemote(a *app.App, _ *model.CommandArgs, arg *model.AutocompleteArg) ([]model.AutocompleteListItem, error) {
|
||||
switch arg.Name {
|
||||
case "remoteId":
|
||||
return getRemoteClusterAutocompleteListItems(a, true)
|
||||
default:
|
||||
return nil, fmt.Errorf("%s not a dynamic argument", arg.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (sp *ShareProvider) DoCommand(a *app.App, args *model.CommandArgs, message string) *model.CommandResponse {
|
||||
if !a.HasPermissionTo(args.UserId, model.PERMISSION_MANAGE_SHARED_CHANNELS) {
|
||||
return responsef(args.T("api.command_share.permission_required", map[string]interface{}{"Permission": "manage_shared_channels"}))
|
||||
}
|
||||
|
||||
if a.Srv().GetSharedChannelSyncService() == nil {
|
||||
return responsef(args.T("api.command_share.service_disabled"))
|
||||
}
|
||||
|
||||
if a.Srv().GetRemoteClusterService() == nil {
|
||||
return responsef(args.T("api.command_remote.service_disabled"))
|
||||
}
|
||||
|
||||
margs := parseNamedArgs(args.Command)
|
||||
action, ok := margs[ActionKey]
|
||||
if !ok {
|
||||
return responsef(args.T("api.command_share.missing_action", map[string]interface{}{"Actions": AvailableShareActions}))
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "share_channel":
|
||||
return sp.doShareChannel(a, args, margs)
|
||||
case "unshare_channel":
|
||||
return sp.doUnshareChannel(a, args, margs)
|
||||
case "invite_remote":
|
||||
return sp.doInviteRemote(a, args, margs)
|
||||
case "uninvite_remote":
|
||||
return sp.doUninviteRemote(a, args, margs)
|
||||
case "status":
|
||||
return sp.doStatus(a, args, margs)
|
||||
}
|
||||
return responsef(args.T("api.command_share.unknown_action", map[string]interface{}{"Action": action, "Actions": AvailableShareActions}))
|
||||
}
|
||||
|
||||
func (sp *ShareProvider) doShareChannel(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
|
||||
// check that channel exists.
|
||||
channel, errApp := a.GetChannel(args.ChannelId)
|
||||
if errApp != nil {
|
||||
return responsef(args.T("api.command_share.share_channel.error", map[string]interface{}{"Error": errApp.Error()}))
|
||||
}
|
||||
|
||||
if name := margs["name"]; name == "" {
|
||||
margs["name"] = channel.Name
|
||||
}
|
||||
if name := margs["displayname"]; name == "" {
|
||||
margs["displayname"] = channel.DisplayName
|
||||
}
|
||||
if name := margs["purpose"]; name == "" {
|
||||
margs["purpose"] = channel.Purpose
|
||||
}
|
||||
if name := margs["header"]; name == "" {
|
||||
margs["header"] = channel.Header
|
||||
}
|
||||
if _, ok := margs["readonly"]; !ok {
|
||||
margs["readonly"] = "N"
|
||||
}
|
||||
|
||||
readonly, err := parseBool(margs["readonly"])
|
||||
if err != nil {
|
||||
return responsef(args.T("api.command_share.invalid_value.error", map[string]interface{}{"Arg": "readonly", "Error": err.Error()}))
|
||||
}
|
||||
|
||||
sc := &model.SharedChannel{
|
||||
ChannelId: args.ChannelId,
|
||||
TeamId: args.TeamId,
|
||||
Home: true,
|
||||
ReadOnly: readonly,
|
||||
ShareName: margs["name"],
|
||||
ShareDisplayName: margs["displayname"],
|
||||
SharePurpose: margs["purpose"],
|
||||
ShareHeader: margs["header"],
|
||||
CreatorId: args.UserId,
|
||||
}
|
||||
|
||||
if _, err := a.SaveSharedChannel(sc); err != nil {
|
||||
return responsef(args.T("api.command_share.share_channel.error", map[string]interface{}{"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 {
|
||||
if _, ok := margs["are_you_sure"]; !ok {
|
||||
margs["are_you_sure"] = "N"
|
||||
}
|
||||
|
||||
sure, err := parseBool(margs["are_you_sure"])
|
||||
if err != nil || !sure {
|
||||
return responsef(args.T("api.command_share.shared_channel_not_deleted", map[string]interface{}{"Arg": "are_you_sure", "Expected": "Y"}))
|
||||
}
|
||||
|
||||
sc, appErr := a.GetSharedChannel(args.ChannelId)
|
||||
if appErr != nil {
|
||||
return responsef(args.T("api.command_share.shared_channel_unshare.error", map[string]interface{}{"Error": appErr.Error()}))
|
||||
}
|
||||
|
||||
deleted, err := a.DeleteSharedChannel(args.ChannelId)
|
||||
if err != nil {
|
||||
return responsef(args.T("api.command_share.shared_channel_unshare.error", map[string]interface{}{"Error": err.Error()}))
|
||||
}
|
||||
if !deleted {
|
||||
return responsef(args.T("api.command_share.not_shared_channel_unshare"))
|
||||
}
|
||||
|
||||
notifyClientsForChannelUpdate(a, sc)
|
||||
|
||||
return responsef("##### " + args.T("api.command_share.shared_channel_unavailable"))
|
||||
}
|
||||
|
||||
func (sp *ShareProvider) doInviteRemote(a *app.App, args *model.CommandArgs, margs map[string]string) (resp *model.CommandResponse) {
|
||||
remoteId, ok := margs["remoteId"]
|
||||
if !ok || remoteId == "" {
|
||||
return responsef(args.T("api.command_share.must_specify_valid_remote"))
|
||||
}
|
||||
|
||||
hasRemote, err := a.HasRemote(args.ChannelId, remoteId)
|
||||
if err != nil {
|
||||
return responsef(args.T("api.command_share.fetch_remote.error", map[string]interface{}{"Error": err.Error()}))
|
||||
}
|
||||
if hasRemote {
|
||||
return responsef(args.T("api.command_share.remote_already_invited"))
|
||||
}
|
||||
|
||||
// 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]interface{}{"Error": err.Error()}))
|
||||
}
|
||||
if !hasChan {
|
||||
// If it doesn't exist, then create it.
|
||||
resp2 := sp.doShareChannel(a, args, margs)
|
||||
// We modify the outgoing response by prepending the text
|
||||
// from the shareChannel response.
|
||||
defer func() {
|
||||
resp.Text = resp2.Text + "\n" + resp.Text
|
||||
}()
|
||||
}
|
||||
|
||||
// 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]interface{}{"Error": appErr.Error()}))
|
||||
}
|
||||
|
||||
channel, errApp := a.GetChannel(args.ChannelId)
|
||||
if errApp != nil {
|
||||
return responsef(args.T("api.command_share.channel_invite.error", map[string]interface{}{"Name": rc.DisplayName, "Error": errApp.Error()}))
|
||||
}
|
||||
// send channel invite to remote cluster
|
||||
if err := a.Srv().GetSharedChannelSyncService().SendChannelInvite(channel, args.UserId, margs["description"], rc); err != nil {
|
||||
return responsef(args.T("api.command_share.channel_invite.error", map[string]interface{}{"Name": rc.DisplayName, "Error": err.Error()}))
|
||||
}
|
||||
|
||||
return responsef("##### " + args.T("api.command_share.invitation_sent", map[string]interface{}{"Name": rc.DisplayName, "SiteURL": rc.SiteURL}))
|
||||
}
|
||||
|
||||
func (sp *ShareProvider) doUninviteRemote(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
|
||||
remoteId, ok := margs["remoteId"]
|
||||
if !ok || remoteId == "" {
|
||||
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]interface{}{"RemoteId": remoteId}))
|
||||
}
|
||||
|
||||
deleted, err := a.DeleteSharedChannelRemote(scr.Id)
|
||||
if err != nil || !deleted {
|
||||
return responsef(args.T("api.command_share.could_not_uninvite.error", map[string]interface{}{"RemoteId": remoteId, "Error": err.Error()}))
|
||||
}
|
||||
return responsef("##### " + args.T("api.command_share.remote_uninvited", map[string]interface{}{"RemoteId": remoteId}))
|
||||
}
|
||||
|
||||
func (sp *ShareProvider) doStatus(a *app.App, args *model.CommandArgs, _ map[string]string) *model.CommandResponse {
|
||||
statuses, err := a.GetSharedChannelRemotesStatus(args.ChannelId)
|
||||
if err != nil {
|
||||
return responsef(args.T("api.command_share.fetch_remote_status.error", map[string]interface{}{"Error": err.Error()}))
|
||||
}
|
||||
if len(statuses) == 0 {
|
||||
return responsef(args.T("api.command_share.no_remote_invited"))
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
fmt.Fprintf(&sb, args.T("api.command_share.channel_status_id", map[string]interface{}{"ChannelId": statuses[0].ChannelId})+"\n\n")
|
||||
|
||||
fmt.Fprintf(&sb, args.T("api.command_share.remote_table_header")+" \n")
|
||||
fmt.Fprintf(&sb, "| ------ | ------- | ----------- | -------- | -------------- | ------ | --------- | \n")
|
||||
|
||||
for _, status := range statuses {
|
||||
online := ":white_check_mark:"
|
||||
if !isOnline(status.LastPingAt) {
|
||||
online = ":skull_and_crossbones:"
|
||||
}
|
||||
|
||||
lastSync := formatTimestamp(model.GetTimeForMillis(status.NextSyncAt))
|
||||
|
||||
fmt.Fprintf(&sb, "| %s | %s | %s | %t | %t | %s | %s |\n",
|
||||
status.DisplayName, status.SiteURL, status.Description,
|
||||
status.ReadOnly, status.IsInviteAccepted, online, lastSync)
|
||||
}
|
||||
return responsef(sb.String())
|
||||
}
|
||||
|
||||
func notifyClientsForChannelUpdate(a *app.App, sharedChannel *model.SharedChannel) {
|
||||
messageWs := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_CONVERTED, sharedChannel.TeamId, "", "", nil)
|
||||
messageWs.Add("channel_id", sharedChannel.ChannelId)
|
||||
a.Publish(messageWs)
|
||||
}
|
||||
92
app/slashcommands/command_share_test.go
Обычный файл
92
app/slashcommands/command_share_test.go
Обычный файл
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package slashcommands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/testlib"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/app"
|
||||
"github.com/mattermost/mattermost-server/v5/services/remotecluster"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
)
|
||||
|
||||
func TestShareProviderDoCommand(t *testing.T) {
|
||||
t.Run("share command sends a websocket channel converted event", func(t *testing.T) {
|
||||
th := setup(t).initBasic()
|
||||
defer th.tearDown()
|
||||
|
||||
th.addPermissionToRole(model.PERMISSION_MANAGE_SHARED_CHANNELS.Id, th.BasicUser.Roles)
|
||||
|
||||
mockSyncService := app.NewMockSharedChannelService(nil)
|
||||
th.Server.SetSharedChannelSyncService(mockSyncService)
|
||||
mockRemoteCluster, err := remotecluster.NewRemoteClusterService(th.Server)
|
||||
require.NoError(t, err)
|
||||
|
||||
th.Server.SetRemoteClusterService(mockRemoteCluster)
|
||||
testCluster := &testlib.FakeClusterInterface{}
|
||||
th.Server.Cluster = testCluster
|
||||
|
||||
commandProvider := ShareProvider{}
|
||||
channel := th.CreateChannel(th.BasicTeam, WithShared(false))
|
||||
args := &model.CommandArgs{
|
||||
T: func(s string, args ...interface{}) string { return s },
|
||||
ChannelId: channel.Id,
|
||||
UserId: th.BasicUser.Id,
|
||||
TeamId: th.BasicTeam.Id,
|
||||
Command: "/share share_channel",
|
||||
}
|
||||
|
||||
response := commandProvider.DoCommand(th.App, args, "")
|
||||
require.Equal(t, "##### "+args.T("api.command_share.channel_shared"), response.Text)
|
||||
|
||||
channelConvertedMessages := testCluster.SelectMessages(func(msg *model.ClusterMessage) bool {
|
||||
event := model.WebSocketEventFromJson(strings.NewReader(msg.Data))
|
||||
return event != nil && event.EventType() == model.WEBSOCKET_EVENT_CHANNEL_CONVERTED
|
||||
})
|
||||
assert.Len(t, channelConvertedMessages, 1)
|
||||
})
|
||||
|
||||
t.Run("unshare command sends a websocket channel converted event", func(t *testing.T) {
|
||||
th := setup(t).initBasic()
|
||||
defer th.tearDown()
|
||||
|
||||
th.addPermissionToRole(model.PERMISSION_MANAGE_SHARED_CHANNELS.Id, th.BasicUser.Roles)
|
||||
|
||||
mockSyncService := app.NewMockSharedChannelService(nil)
|
||||
th.Server.SetSharedChannelSyncService(mockSyncService)
|
||||
mockRemoteCluster, err := remotecluster.NewRemoteClusterService(th.Server)
|
||||
require.NoError(t, err)
|
||||
|
||||
th.Server.SetRemoteClusterService(mockRemoteCluster)
|
||||
testCluster := &testlib.FakeClusterInterface{}
|
||||
th.Server.Cluster = testCluster
|
||||
|
||||
commandProvider := ShareProvider{}
|
||||
channel := th.CreateChannel(th.BasicTeam, WithShared(true))
|
||||
args := &model.CommandArgs{
|
||||
T: func(s string, args ...interface{}) string { return s },
|
||||
ChannelId: channel.Id,
|
||||
UserId: th.BasicUser.Id,
|
||||
TeamId: th.BasicTeam.Id,
|
||||
Command: "/share unshare_channel --are_you_sure Y",
|
||||
}
|
||||
|
||||
response := commandProvider.DoCommand(th.App, args, "")
|
||||
require.Equal(t, "##### "+args.T("api.command_share.shared_channel_unavailable"), response.Text)
|
||||
|
||||
channelConvertedMessages := testCluster.SelectMessages(func(msg *model.ClusterMessage) bool {
|
||||
event := model.WebSocketEventFromJson(strings.NewReader(msg.Data))
|
||||
return event != nil && event.EventType() == model.WEBSOCKET_EVENT_CHANNEL_CONVERTED
|
||||
})
|
||||
require.Len(t, channelConvertedMessages, 1)
|
||||
})
|
||||
}
|
||||
@@ -226,15 +226,23 @@ func (th *TestHelper) createUserOrGuest(guest bool) *model.User {
|
||||
return user
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateChannel(team *model.Team) *model.Channel {
|
||||
return th.createChannel(team, model.CHANNEL_OPEN)
|
||||
type ChannelOption func(*model.Channel)
|
||||
|
||||
func WithShared(v bool) ChannelOption {
|
||||
return func(channel *model.Channel) {
|
||||
channel.Shared = model.NewBool(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateChannel(team *model.Team, options ...ChannelOption) *model.Channel {
|
||||
return th.createChannel(team, model.CHANNEL_OPEN, options...)
|
||||
}
|
||||
|
||||
func (th *TestHelper) createPrivateChannel(team *model.Team) *model.Channel {
|
||||
return th.createChannel(team, model.CHANNEL_PRIVATE)
|
||||
}
|
||||
|
||||
func (th *TestHelper) createChannel(team *model.Team, channelType string) *model.Channel {
|
||||
func (th *TestHelper) createChannel(team *model.Team, channelType string, options ...ChannelOption) *model.Channel {
|
||||
id := model.NewId()
|
||||
|
||||
channel := &model.Channel{
|
||||
@@ -245,11 +253,32 @@ func (th *TestHelper) createChannel(team *model.Team, channelType string) *model
|
||||
CreatorId: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(channel)
|
||||
}
|
||||
|
||||
utils.DisableDebugLogForTest()
|
||||
var err *model.AppError
|
||||
if channel, err = th.App.CreateChannel(channel, true); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if channel.IsShared() {
|
||||
id := model.NewId()
|
||||
_, err := th.App.SaveSharedChannel(&model.SharedChannel{
|
||||
ChannelId: channel.Id,
|
||||
TeamId: channel.TeamId,
|
||||
Home: false,
|
||||
ReadOnly: false,
|
||||
ShareName: "shared-" + id,
|
||||
ShareDisplayName: "shared-" + id,
|
||||
CreatorId: th.BasicUser.Id,
|
||||
RemoteId: model.NewId(),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
utils.EnableDebugLogForTest()
|
||||
return channel
|
||||
}
|
||||
|
||||
88
app/slashcommands/util.go
Обычный файл
88
app/slashcommands/util.go
Обычный файл
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package slashcommands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
)
|
||||
|
||||
const (
|
||||
ActionKey = "-action"
|
||||
)
|
||||
|
||||
// responsef creates an ephemeral command response using printf syntax.
|
||||
func responsef(format string, args ...interface{}) *model.CommandResponse {
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL,
|
||||
Text: fmt.Sprintf(format, args...),
|
||||
Type: model.POST_DEFAULT,
|
||||
}
|
||||
}
|
||||
|
||||
// parseNamedArgs parses a command string into a map of arguments. It is assumed the
|
||||
// command string is of the form `<action> --arg1 value1 ...` Supports empty values.
|
||||
// Arg names are limited to [0-9a-zA-Z_].
|
||||
func parseNamedArgs(cmd string) map[string]string {
|
||||
m := make(map[string]string)
|
||||
|
||||
split := strings.Fields(cmd)
|
||||
|
||||
// check for optional action
|
||||
if len(split) >= 2 && !strings.HasPrefix(split[1], "--") {
|
||||
m[ActionKey] = split[1] // prefix with hyphen to avoid collision with arg named "action"
|
||||
}
|
||||
|
||||
for i := 0; i < len(split); i++ {
|
||||
if !strings.HasPrefix(split[i], "--") {
|
||||
continue
|
||||
}
|
||||
var val string
|
||||
arg := trimSpaceAndQuotes(strings.Trim(split[i], "-"))
|
||||
if i < len(split)-1 && !strings.HasPrefix(split[i+1], "--") {
|
||||
val = trimSpaceAndQuotes(split[i+1])
|
||||
}
|
||||
if arg != "" {
|
||||
m[arg] = val
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func trimSpaceAndQuotes(s string) string {
|
||||
trimmed := strings.TrimSpace(s)
|
||||
trimmed = strings.TrimPrefix(trimmed, "\"")
|
||||
trimmed = strings.TrimPrefix(trimmed, "'")
|
||||
trimmed = strings.TrimSuffix(trimmed, "\"")
|
||||
trimmed = strings.TrimSuffix(trimmed, "'")
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func parseBool(s string) (bool, error) {
|
||||
switch strings.ToLower(s) {
|
||||
case "1", "t", "true", "yes", "y":
|
||||
return true, nil
|
||||
case "0", "f", "false", "no", "n":
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("cannot parse '%s' as a boolean", s)
|
||||
}
|
||||
|
||||
func formatTimestamp(ts time.Time) string {
|
||||
if !isToday(ts) {
|
||||
return ts.Format("Jan 2 15:04:05 MST 2006")
|
||||
}
|
||||
date := ts.Format("15:04:05 MST 2006")
|
||||
return fmt.Sprintf("today %s", date)
|
||||
}
|
||||
|
||||
func isToday(ts time.Time) bool {
|
||||
now := time.Now()
|
||||
year, month, day := ts.Date()
|
||||
nowYear, nowMonth, nowDay := now.Date()
|
||||
return year == nowYear && month == nowMonth && day == nowDay
|
||||
}
|
||||
40
app/slashcommands/util_test.go
Обычный файл
40
app/slashcommands/util_test.go
Обычный файл
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package slashcommands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseNamedArgs(t *testing.T) {
|
||||
data := []struct {
|
||||
name string
|
||||
s string
|
||||
m map[string]string
|
||||
}{
|
||||
{"empty", "", map[string]string{}},
|
||||
{"gibberish", "ifu3ue-h29f8", map[string]string{}},
|
||||
{"action only", "remote status", map[string]string{ActionKey: "status"}},
|
||||
{"no action", "remote --arg1 val1 --arg2 val2", map[string]string{"arg1": "val1", "arg2": "val2"}},
|
||||
{"command only", "remote", map[string]string{}},
|
||||
{"trailing empty arg", "remote add --arg1 val1 --arg2", map[string]string{ActionKey: "add", "arg1": "val1", "arg2": ""}},
|
||||
{"leading empty arg", "remote add --arg1 --arg2 val2", map[string]string{ActionKey: "add", "arg1": "", "arg2": "val2"}},
|
||||
{"weird", "-- -- -- --", map[string]string{}},
|
||||
{"hyphen before action", "remote -- add", map[string]string{}},
|
||||
{"trailing hyphen", "remote add -- ", map[string]string{ActionKey: "add"}},
|
||||
{"hyphen in val", "remote add --arg1 val-1 ", map[string]string{ActionKey: "add", "arg1": "val-1"}},
|
||||
{"quote prefix and suffix", "remote add --arg1 \"val-1\"", map[string]string{ActionKey: "add", "arg1": "val-1"}},
|
||||
{"quote embedded", "remote add --arg1 O'Brien", map[string]string{ActionKey: "add", "arg1": "O'Brien"}},
|
||||
{"quote prefix, suffix, and embedded", "remote add --arg1 \"O'Brien\"", map[string]string{ActionKey: "add", "arg1": "O'Brien"}},
|
||||
{"empty quotes", "remote add --arg1 \"\"", map[string]string{ActionKey: "add", "arg1": ""}},
|
||||
}
|
||||
|
||||
for _, tt := range data {
|
||||
m := parseNamedArgs(tt.s)
|
||||
assert.NotNil(t, m)
|
||||
assert.Equal(t, tt.m, m, tt.name)
|
||||
}
|
||||
}
|
||||
@@ -253,6 +253,10 @@ func (a *App) UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo
|
||||
|
||||
info.CreatorId = us.UserId
|
||||
info.Path = us.Path
|
||||
info.RemoteId = model.NewString(us.RemoteId)
|
||||
if us.ReqFileId != "" {
|
||||
info.Id = us.ReqFileId
|
||||
}
|
||||
|
||||
// run plugins upload hook
|
||||
if err := a.runPluginsHook(info, file); err != nil {
|
||||
|
||||
@@ -180,17 +180,20 @@ func (a *App) Publish(message *model.WebSocketEvent) {
|
||||
a.Srv().Publish(message)
|
||||
}
|
||||
|
||||
func (s *Server) PublishSkipClusterSend(message *model.WebSocketEvent) {
|
||||
if message.GetBroadcast().UserId != "" {
|
||||
hub := s.GetHubForUserId(message.GetBroadcast().UserId)
|
||||
func (s *Server) PublishSkipClusterSend(event *model.WebSocketEvent) {
|
||||
if event.GetBroadcast().UserId != "" {
|
||||
hub := s.GetHubForUserId(event.GetBroadcast().UserId)
|
||||
if hub != nil {
|
||||
hub.Broadcast(message)
|
||||
hub.Broadcast(event)
|
||||
}
|
||||
} else {
|
||||
for _, hub := range s.hubs {
|
||||
hub.Broadcast(message)
|
||||
hub.Broadcast(event)
|
||||
}
|
||||
}
|
||||
|
||||
// Notify shared channel sync service
|
||||
s.SharedChannelSyncHandler(event)
|
||||
}
|
||||
|
||||
func (a *App) invalidateCacheForChannel(channel *model.Channel) {
|
||||
|
||||
Ссылка в новой задаче
Block a user