Move playbooks to plugin (#23732)
* Remove build references * Remove playbooks webapp and server, and add the prepackaged plugin * Remove translations * Add ProductSettings to the playwright type * Restore playbooks as a prepackaged plugin for cypress e2e tests
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
f386ef1ea8
Коммит
44a99d1736
@@ -148,6 +148,7 @@ const prepackagedPlugins = [
|
|||||||
'com.mattermost.nps',
|
'com.mattermost.nps',
|
||||||
'com.mattermost.welcomebot',
|
'com.mattermost.welcomebot',
|
||||||
'zoom',
|
'zoom',
|
||||||
|
'playbooks',
|
||||||
];
|
];
|
||||||
|
|
||||||
Cypress.Commands.add('apiDisableNonPrepackagedPlugins', () => {
|
Cypress.Commands.add('apiDisableNonPrepackagedPlugins', () => {
|
||||||
|
|||||||
@@ -606,9 +606,7 @@ const defaultServerConfig: AdminConfig = {
|
|||||||
CleanupJobsThresholdDays: -1,
|
CleanupJobsThresholdDays: -1,
|
||||||
CleanupConfigThresholdDays: -1,
|
CleanupConfigThresholdDays: -1,
|
||||||
},
|
},
|
||||||
ProductSettings: {
|
ProductSettings: {},
|
||||||
EnablePlaybooks: true,
|
|
||||||
},
|
|
||||||
PluginSettings: {
|
PluginSettings: {
|
||||||
Enable: true,
|
Enable: true,
|
||||||
EnableUploads: false,
|
EnableUploads: false,
|
||||||
|
|||||||
@@ -147,8 +147,7 @@ DIST_PATH_WIN=$(DIST_ROOT)/windows/mattermost
|
|||||||
TESTS=.
|
TESTS=.
|
||||||
|
|
||||||
# Packages lists
|
# Packages lists
|
||||||
TE_PACKAGES=$(shell $(GO) list ./... | grep -vE 'server/v8/playbooks|server/v8/cmd/mmctl')
|
TE_PACKAGES=$(shell $(GO) list ./... | grep -vE 'server/v8/cmd/mmctl')
|
||||||
PLAYBOOKS_PACKAGES=$(shell $(GO) list ./... | grep -E 'server/v8/playbooks')
|
|
||||||
SUITE_PACKAGES=$(shell $(GO) list ./...| grep -vE 'server/v8/cmd/mmctl')
|
SUITE_PACKAGES=$(shell $(GO) list ./...| grep -vE 'server/v8/cmd/mmctl')
|
||||||
MMCTL_PACKAGES=$(shell $(GO) list ./... | grep -E 'server/v8/cmd/mmctl')
|
MMCTL_PACKAGES=$(shell $(GO) list ./... | grep -E 'server/v8/cmd/mmctl')
|
||||||
|
|
||||||
@@ -164,6 +163,7 @@ PLUGIN_PACKAGES += mattermost-plugin-confluence-v1.3.0
|
|||||||
PLUGIN_PACKAGES += mattermost-plugin-custom-attributes-v1.3.1
|
PLUGIN_PACKAGES += mattermost-plugin-custom-attributes-v1.3.1
|
||||||
PLUGIN_PACKAGES += mattermost-plugin-github-v2.1.6
|
PLUGIN_PACKAGES += mattermost-plugin-github-v2.1.6
|
||||||
PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.6.0
|
PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.6.0
|
||||||
|
PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.36.1
|
||||||
PLUGIN_PACKAGES += mattermost-plugin-jenkins-v1.1.0
|
PLUGIN_PACKAGES += mattermost-plugin-jenkins-v1.1.0
|
||||||
PLUGIN_PACKAGES += mattermost-plugin-jira-v3.2.5
|
PLUGIN_PACKAGES += mattermost-plugin-jira-v3.2.5
|
||||||
PLUGIN_PACKAGES += mattermost-plugin-jitsi-v2.0.1
|
PLUGIN_PACKAGES += mattermost-plugin-jitsi-v2.0.1
|
||||||
@@ -188,9 +188,9 @@ endif
|
|||||||
EE_PACKAGES=$(shell $(GO) list $(BUILD_ENTERPRISE_DIR)/...)
|
EE_PACKAGES=$(shell $(GO) list $(BUILD_ENTERPRISE_DIR)/...)
|
||||||
|
|
||||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||||
ALL_PACKAGES=$(TE_PACKAGES) $(PLAYBOOKS_PACKAGES) $(EE_PACKAGES)
|
ALL_PACKAGES=$(TE_PACKAGES) $(EE_PACKAGES)
|
||||||
else
|
else
|
||||||
ALL_PACKAGES=$(TE_PACKAGES) $(PLAYBOOKS_PACKAGES)
|
ALL_PACKAGES=$(TE_PACKAGES)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
all: run ## Alias for 'run'.
|
all: run ## Alias for 'run'.
|
||||||
@@ -460,7 +460,6 @@ endif
|
|||||||
|
|
||||||
test-server-race: test-server-pre
|
test-server-race: test-server-pre
|
||||||
./scripts/test.sh "$(GO)" "-race $(GOFLAGS)" "$(TE_PACKAGES) $(EE_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "90m"
|
./scripts/test.sh "$(GO)" "-race $(GOFLAGS)" "$(TE_PACKAGES) $(EE_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "90m"
|
||||||
./scripts/test.sh "$(GO)" "-race $(GOFLAGS)" "$(PLAYBOOKS_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "90m"
|
|
||||||
ifneq ($(IS_CI),true)
|
ifneq ($(IS_CI),true)
|
||||||
ifneq ($(MM_NO_DOCKER),true)
|
ifneq ($(MM_NO_DOCKER),true)
|
||||||
ifneq ($(TEMP_DOCKER_SERVICES),)
|
ifneq ($(TEMP_DOCKER_SERVICES),)
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ func TestInstallPluginLocally(t *testing.T) {
|
|||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
cleanExistingBundles(t, th)
|
cleanExistingBundles(t, th)
|
||||||
|
|
||||||
_, appErr := installPlugin(t, th, "playbooks", "0.0.1", installPluginLocallyAlways)
|
_, appErr := installPlugin(t, th, "com.mattermost.plugin-incident-response", "0.0.1", installPluginLocallyAlways)
|
||||||
require.NotNil(t, appErr)
|
require.NotNil(t, appErr)
|
||||||
require.Equal(t, "app.plugin.blocked.app_error", appErr.Id)
|
require.Equal(t, "app.plugin.blocked.app_error", appErr.Id)
|
||||||
assertBundleInfoManifests(t, th, []*model.Manifest{})
|
assertBundleInfoManifests(t, th, []*model.Manifest{})
|
||||||
|
|||||||
@@ -83,16 +83,6 @@ func (s *Server) shouldStart(product string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if product == "playbooks" {
|
|
||||||
if os.Getenv("MM_DISABLE_PLAYBOOKS") == "true" {
|
|
||||||
s.Log().Warn("Skipping Playbooks start: disabled via env var")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !*s.Config().ProductSettings.EnablePlaybooks {
|
|
||||||
s.Log().Warn("Skipping Playbooks start: disabled via configuration")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
|
|
||||||
// Blank imports for each product to register themselves
|
// Blank imports for each product to register themselves
|
||||||
_ "github.com/mattermost/mattermost/server/v8/boards/product"
|
_ "github.com/mattermost/mattermost/server/v8/boards/product"
|
||||||
_ "github.com/mattermost/mattermost/server/v8/playbooks/product"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -144,8 +144,6 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
|
|||||||
props["AllowSyncedDrafts"] = strconv.FormatBool(*c.ServiceSettings.AllowSyncedDrafts)
|
props["AllowSyncedDrafts"] = strconv.FormatBool(*c.ServiceSettings.AllowSyncedDrafts)
|
||||||
props["DelayChannelAutocomplete"] = strconv.FormatBool(*c.ExperimentalSettings.DelayChannelAutocomplete)
|
props["DelayChannelAutocomplete"] = strconv.FormatBool(*c.ExperimentalSettings.DelayChannelAutocomplete)
|
||||||
|
|
||||||
props["EnablePlaybooks"] = strconv.FormatBool(*c.ProductSettings.EnablePlaybooks)
|
|
||||||
|
|
||||||
if license != nil {
|
if license != nil {
|
||||||
props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer)
|
props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer)
|
||||||
|
|
||||||
|
|||||||
@@ -326,20 +326,6 @@ func TestGetClientConfig(t *testing.T) {
|
|||||||
"ExperimentalSharedChannels": "true",
|
"ExperimentalSharedChannels": "true",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"Default Playbooks Enabled",
|
|
||||||
&model.Config{
|
|
||||||
ProductSettings: model.ProductSettings{},
|
|
||||||
},
|
|
||||||
"",
|
|
||||||
&model.License{
|
|
||||||
Features: &model.Features{},
|
|
||||||
SkuShortName: "other",
|
|
||||||
},
|
|
||||||
map[string]string{
|
|
||||||
"EnablePlaybooks": "true",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
|
|||||||
@@ -810,6 +810,9 @@ func TestDiff(t *testing.T) {
|
|||||||
"com.mattermost.calls": {
|
"com.mattermost.calls": {
|
||||||
Enable: true,
|
Enable: true,
|
||||||
},
|
},
|
||||||
|
"playbooks": {
|
||||||
|
Enable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -839,6 +842,9 @@ func TestDiff(t *testing.T) {
|
|||||||
"com.mattermost.calls": {
|
"com.mattermost.calls": {
|
||||||
Enable: true,
|
Enable: true,
|
||||||
},
|
},
|
||||||
|
"playbooks": {
|
||||||
|
Enable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -860,6 +866,9 @@ func TestDiff(t *testing.T) {
|
|||||||
"com.mattermost.calls": {
|
"com.mattermost.calls": {
|
||||||
Enable: true,
|
Enable: true,
|
||||||
},
|
},
|
||||||
|
"playbooks": {
|
||||||
|
Enable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,13 +22,11 @@ require (
|
|||||||
github.com/golang-migrate/migrate/v4 v4.15.2
|
github.com/golang-migrate/migrate/v4 v4.15.2
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/google/go-querystring v1.1.0
|
|
||||||
github.com/gorilla/handlers v1.5.1
|
github.com/gorilla/handlers v1.5.1
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/gorilla/schema v1.2.0
|
github.com/gorilla/schema v1.2.0
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/graph-gophers/dataloader/v6 v6.0.0
|
github.com/graph-gophers/dataloader/v6 v6.0.0
|
||||||
github.com/graph-gophers/dataloader/v7 v7.1.0
|
|
||||||
github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a
|
github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a
|
||||||
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c
|
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c
|
||||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
|
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
|
||||||
@@ -54,7 +52,6 @@ require (
|
|||||||
github.com/mholt/archiver/v3 v3.5.1
|
github.com/mholt/archiver/v3 v3.5.1
|
||||||
github.com/microcosm-cc/bluemonday v1.0.23
|
github.com/microcosm-cc/bluemonday v1.0.23
|
||||||
github.com/minio/minio-go/v7 v7.0.51
|
github.com/minio/minio-go/v7 v7.0.51
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
|
||||||
github.com/oklog/run v1.1.0
|
github.com/oklog/run v1.1.0
|
||||||
github.com/oov/psd v0.0.0-20220121172623-5db5eafcecbb
|
github.com/oov/psd v0.0.0-20220121172623-5db5eafcecbb
|
||||||
github.com/opentracing/opentracing-go v1.2.0
|
github.com/opentracing/opentracing-go v1.2.0
|
||||||
@@ -78,17 +75,14 @@ require (
|
|||||||
github.com/uber/jaeger-lib v2.4.1+incompatible
|
github.com/uber/jaeger-lib v2.4.1+incompatible
|
||||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||||
github.com/wiggin77/merror v1.0.4
|
github.com/wiggin77/merror v1.0.4
|
||||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
|
||||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c
|
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c
|
||||||
github.com/yuin/goldmark v1.5.4
|
github.com/yuin/goldmark v1.5.4
|
||||||
golang.org/x/crypto v0.8.0
|
golang.org/x/crypto v0.8.0
|
||||||
golang.org/x/image v0.7.0
|
golang.org/x/image v0.7.0
|
||||||
golang.org/x/net v0.10.0
|
golang.org/x/net v0.10.0
|
||||||
golang.org/x/oauth2 v0.7.0
|
|
||||||
golang.org/x/sync v0.2.0
|
golang.org/x/sync v0.2.0
|
||||||
golang.org/x/term v0.8.0
|
golang.org/x/term v0.8.0
|
||||||
golang.org/x/tools v0.9.1
|
golang.org/x/tools v0.9.1
|
||||||
gopkg.in/guregu/null.v4 v4.0.0
|
|
||||||
gopkg.in/mail.v2 v2.3.1
|
gopkg.in/mail.v2 v2.3.1
|
||||||
gopkg.in/olivere/elastic.v6 v6.2.37
|
gopkg.in/olivere/elastic.v6 v6.2.37
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
@@ -178,6 +172,7 @@ require (
|
|||||||
github.com/minio/md5-simd v1.1.2 // indirect
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/mschoch/smat v0.2.0 // indirect
|
github.com/mschoch/smat v0.2.0 // indirect
|
||||||
@@ -224,7 +219,6 @@ require (
|
|||||||
golang.org/x/mod v0.10.0 // indirect
|
golang.org/x/mod v0.10.0 // indirect
|
||||||
golang.org/x/sys v0.8.0 // indirect
|
golang.org/x/sys v0.8.0 // indirect
|
||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/text v0.9.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||||
google.golang.org/grpc v1.54.0 // indirect
|
google.golang.org/grpc v1.54.0 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
|
|||||||
@@ -722,7 +722,6 @@ github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYV
|
|||||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||||
github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=
|
github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=
|
||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@@ -787,8 +786,6 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
|
|||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/graph-gophers/dataloader/v6 v6.0.0 h1:qBpmq3B8PIQesoh0EJXKGfw+ulMUb+KFl4IZOe9ScWg=
|
github.com/graph-gophers/dataloader/v6 v6.0.0 h1:qBpmq3B8PIQesoh0EJXKGfw+ulMUb+KFl4IZOe9ScWg=
|
||||||
github.com/graph-gophers/dataloader/v6 v6.0.0/go.mod h1:J15OZSnOoZgMkijpbZcwCmglIDYqlUiTEE1xLPbyqZM=
|
github.com/graph-gophers/dataloader/v6 v6.0.0/go.mod h1:J15OZSnOoZgMkijpbZcwCmglIDYqlUiTEE1xLPbyqZM=
|
||||||
github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc=
|
|
||||||
github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q=
|
|
||||||
github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a h1:i0+Se9S+2zL5CBxJouqn2Ej6UQMwH1c57ZB6DVnqck4=
|
github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a h1:i0+Se9S+2zL5CBxJouqn2Ej6UQMwH1c57ZB6DVnqck4=
|
||||||
github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os=
|
github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os=
|
||||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||||
@@ -1559,8 +1556,6 @@ github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8
|
|||||||
github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls=
|
github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls=
|
||||||
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
|
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
|
||||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw=
|
|
||||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE=
|
|
||||||
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
|
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||||
@@ -1841,8 +1836,6 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ
|
|||||||
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
|
|
||||||
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
|
|
||||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -2181,7 +2174,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
|||||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
|
||||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
|
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
@@ -2336,8 +2328,6 @@ gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS
|
|||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg=
|
|
||||||
gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI=
|
|
||||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
|||||||
@@ -4947,10 +4947,6 @@
|
|||||||
"id": "app.command.deletecommand.internal_error",
|
"id": "app.command.deletecommand.internal_error",
|
||||||
"translation": "Unable to delete the command."
|
"translation": "Unable to delete the command."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.command.execute.error",
|
|
||||||
"translation": "Unable to execute command."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "app.command.getcommand.internal_error",
|
"id": "app.command.getcommand.internal_error",
|
||||||
"translation": "Unable to get the command."
|
"translation": "Unable to get the command."
|
||||||
@@ -6815,88 +6811,6 @@
|
|||||||
"id": "app.user.demote_user_to_guest.user_update.app_error",
|
"id": "app.user.demote_user_to_guest.user_update.app_error",
|
||||||
"translation": "Failed to update the user."
|
"translation": "Failed to update the user."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.user.digest.overdue_status_updates.heading",
|
|
||||||
"translation": "Overdue Status Updates"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.overdue_status_updates.num_overdue",
|
|
||||||
"translation": {
|
|
||||||
"one": "You have {{.Count}} run overdue for a status update:",
|
|
||||||
"other": "You have {{.Count}} runs overdue for a status update:"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.overdue_status_updates.zero_overdue",
|
|
||||||
"translation": "You have 0 runs overdue."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.runs_in_progress.heading",
|
|
||||||
"translation": "Runs in Progress"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.runs_in_progress.num_in_progress",
|
|
||||||
"translation": {
|
|
||||||
"one": "You have {{.Count}} run currently in progress:",
|
|
||||||
"other": "You have {{.Count}} runs currently in progress:"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.runs_in_progress.zero_in_progress",
|
|
||||||
"translation": "You have 0 runs currently in progress."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.tasks.all_tasks_command",
|
|
||||||
"translation": "Please use `/playbook todo` to see all your tasks."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.tasks.due_after_today",
|
|
||||||
"translation": {
|
|
||||||
"one": "You have **{{.Count}} assigned task due after today**.",
|
|
||||||
"other": "You have **{{.Count}} assigned tasks due after today**."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.tasks.due_in_x_days",
|
|
||||||
"translation": {
|
|
||||||
"one": "Due in {{.Count}} day",
|
|
||||||
"other": "Due in {{.Count}} days"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.tasks.due_today",
|
|
||||||
"translation": "Due today"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.tasks.due_x_days_ago",
|
|
||||||
"translation": "Due {{.Count}} days ago"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.tasks.due_yesterday",
|
|
||||||
"translation": "Due yesterday"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.tasks.heading",
|
|
||||||
"translation": "Your assigned tasks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.tasks.num_assigned",
|
|
||||||
"translation": {
|
|
||||||
"one": "You have {{.Count}} assigned task:",
|
|
||||||
"other": "You have {{.Count}} total assigned tasks:"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.tasks.num_assigned_due_until_today",
|
|
||||||
"translation": {
|
|
||||||
"one": "You have {{.Count}} assigned task that is now due:",
|
|
||||||
"other": "You have {{.Count}} assigned tasks that are now due:"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.digest.tasks.zero_assigned",
|
|
||||||
"translation": "You have 0 assigned tasks."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "app.user.get.app_error",
|
"id": "app.user.get.app_error",
|
||||||
"translation": "We encountered an error finding the account."
|
"translation": "We encountered an error finding the account."
|
||||||
@@ -6973,26 +6887,6 @@
|
|||||||
"id": "app.user.missing_account.const",
|
"id": "app.user.missing_account.const",
|
||||||
"translation": "Unable to find the user."
|
"translation": "Unable to find the user."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.user.new_run.intro",
|
|
||||||
"translation": "**Owner** {{.Username}}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.new_run.playbook",
|
|
||||||
"translation": "Playbook"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.new_run.run_name",
|
|
||||||
"translation": "Run name"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.new_run.submit_label",
|
|
||||||
"translation": "Start run"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.new_run.title",
|
|
||||||
"translation": "Run playbook"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "app.user.permanent_delete.app_error",
|
"id": "app.user.permanent_delete.app_error",
|
||||||
"translation": "Unable to delete the existing account."
|
"translation": "Unable to delete the existing account."
|
||||||
@@ -7005,108 +6899,6 @@
|
|||||||
"id": "app.user.promote_guest.user_update.app_error",
|
"id": "app.user.promote_guest.user_update.app_error",
|
||||||
"translation": "Failed to update the user."
|
"translation": "Failed to update the user."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.user.run.add_checklist_item.description",
|
|
||||||
"translation": "Description"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.add_checklist_item.name",
|
|
||||||
"translation": "Name"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.add_checklist_item.submit_label",
|
|
||||||
"translation": "Add task"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.add_checklist_item.title",
|
|
||||||
"translation": "Add new task"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.add_to_timeline.playbook_run",
|
|
||||||
"translation": "Playbook Run"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.add_to_timeline.submit_label",
|
|
||||||
"translation": "Add to run timeline"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.add_to_timeline.summary",
|
|
||||||
"translation": "Summary"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.add_to_timeline.summary.help",
|
|
||||||
"translation": "Max 64 chars"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.add_to_timeline.summary.placeholder",
|
|
||||||
"translation": "Short summary shown in the timeline"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.add_to_timeline.title",
|
|
||||||
"translation": "Add to run timeline"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.confirm_finish.num_outstanding",
|
|
||||||
"translation": {
|
|
||||||
"one": "There is **{{.Count}} outstanding task**. Are you sure you want to finish the run *{{.RunName}}* for all participants?",
|
|
||||||
"other": "There are **{{.Count}} outstanding tasks**. Are you sure you want to finish the run *{{.RunName}}* for all participants?"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.confirm_finish.submit_label",
|
|
||||||
"translation": "Finish run"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.confirm_finish.title",
|
|
||||||
"translation": "Confirm finish run"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.request_join_channel",
|
|
||||||
"translation": "@{{.Name}} is a run participant and wants join this channel. Any member of the channel can invite them.\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.request_update",
|
|
||||||
"translation": "@here — @{{.Name}} requested a status update for [{{.RunName}}]({{.RunURL}}). \n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.status_disable",
|
|
||||||
"translation": "@{{.Username}} disabled the status updates for [{{.RunName}}]({{.RunURL}})"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.status_enable",
|
|
||||||
"translation": "@{{.Username}} enabled the status updates for [{{.RunName}}]({{.RunURL}})"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.update_status.change_since_last_update",
|
|
||||||
"translation": "Change since last update"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.update_status.finish_run",
|
|
||||||
"translation": "Finish run"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.update_status.finish_run.placeholder",
|
|
||||||
"translation": "Also mark the run as finished"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.update_status.num_channel",
|
|
||||||
"translation": {
|
|
||||||
"one": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channel.",
|
|
||||||
"other": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channels."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.update_status.reminder_for_next_update",
|
|
||||||
"translation": "Reminder for next update"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.update_status.submit_label",
|
|
||||||
"translation": "Update status"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.user.run.update_status.title",
|
|
||||||
"translation": "Status update"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "app.user.save.app_error",
|
"id": "app.user.save.app_error",
|
||||||
"translation": "Unable to save the account."
|
"translation": "Unable to save the account."
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
type GenericChannelActionWithoutPayload struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
ChannelID string `json:"channel_id"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
DeleteAt int64 `json:"delete_at"`
|
|
||||||
ActionType string `json:"action_type"`
|
|
||||||
TriggerType string `json:"trigger_type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GenericChannelAction struct {
|
|
||||||
GenericChannelActionWithoutPayload
|
|
||||||
Payload interface{} `json:"payload"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WelcomeMessagePayload struct {
|
|
||||||
Message string `json:"message" mapstructure:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PromptRunPlaybookFromKeywordsPayload struct {
|
|
||||||
Keywords []string `json:"keywords" mapstructure:"keywords"`
|
|
||||||
PlaybookID string `json:"playbook_id" mapstructure:"playbook_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CategorizeChannelPayload struct {
|
|
||||||
CategoryName string `json:"category_name" mapstructure:"category_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WelcomeMessageAction struct {
|
|
||||||
GenericChannelActionWithoutPayload
|
|
||||||
Payload WelcomeMessagePayload `json:"payload"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Action types
|
|
||||||
ActionTypeWelcomeMessage = "send_welcome_message"
|
|
||||||
ActionTypePromptRunPlaybook = "prompt_run_playbook"
|
|
||||||
ActionTypeCategorizeChannel = "categorize_channel"
|
|
||||||
|
|
||||||
// Trigger types
|
|
||||||
TriggerTypeNewMemberJoins = "new_member_joins"
|
|
||||||
TriggerTypeKeywordsPosted = "keywords"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ChannelActionListOptions specifies the optional parameters to the
|
|
||||||
// ActionsService.List method.
|
|
||||||
type ChannelActionListOptions struct {
|
|
||||||
TriggerType string `url:"trigger_type,omitempty"`
|
|
||||||
ActionType string `url:"action_type,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChannelActionCreateOptions specifies the parameters for ActionsService.Create method.
|
|
||||||
type ChannelActionCreateOptions struct {
|
|
||||||
ChannelID string `json:"channel_id"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
ActionType string `json:"action_type"`
|
|
||||||
TriggerType string `json:"trigger_type"`
|
|
||||||
Payload interface{} `json:"payload"`
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ActionsService handles communication with the actions related
|
|
||||||
// methods of the Playbook API.
|
|
||||||
type ActionsService struct {
|
|
||||||
client *Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an action. Returns the id of the newly created action.
|
|
||||||
func (s *ActionsService) Create(ctx context.Context, channelID string, opts ChannelActionCreateOptions) (string, error) {
|
|
||||||
actionURL := fmt.Sprintf("actions/channels/%s", channelID)
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, actionURL, opts)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
resp, err := s.client.do(ctx, req, &result)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusCreated {
|
|
||||||
return "", fmt.Errorf("expected status code %d", http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List the actions in a channel.
|
|
||||||
func (s *ActionsService) List(ctx context.Context, channelID string, opts ChannelActionListOptions) ([]GenericChannelAction, error) {
|
|
||||||
actionURL, err := addOptions(fmt.Sprintf("actions/channels/%s", channelID), opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to build options: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := s.client.newRequest(http.MethodGet, actionURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to build request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := []GenericChannelAction{}
|
|
||||||
resp, err := s.client.do(ctx, req, &result)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to execute request: %w", err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update an existing action.
|
|
||||||
func (s *ActionsService) Update(ctx context.Context, action GenericChannelAction) error {
|
|
||||||
updateURL := fmt.Sprintf("actions/channels/%s/%s", action.ChannelID, action.ID)
|
|
||||||
req, err := s.client.newRequest(http.MethodPut, updateURL, action)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/google/go-querystring/query"
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
apiVersion = "v0"
|
|
||||||
manifestID = "playbooks"
|
|
||||||
userAgent = "go-client/" + apiVersion
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client manages communication with the Playbooks API.
|
|
||||||
type Client struct {
|
|
||||||
// client is the underlying HTTP client used to make API requests.
|
|
||||||
client *http.Client
|
|
||||||
// BaseURL is the base HTTP endpoint for the Playbooks plugin.
|
|
||||||
BaseURL *url.URL
|
|
||||||
// User agent used when communicating with the Playbooks API.
|
|
||||||
UserAgent string
|
|
||||||
|
|
||||||
// PlaybookRuns is a collection of methods used to interact with playbook runs.
|
|
||||||
PlaybookRuns *PlaybookRunService
|
|
||||||
// Playbooks is a collection of methods used to interact with playbooks.
|
|
||||||
Playbooks *PlaybooksService
|
|
||||||
// Settings is a collection of methods used to interact with settings.
|
|
||||||
Settings *SettingsService
|
|
||||||
// Actions is a collection of methods used to interact with actions.
|
|
||||||
Actions *ActionsService
|
|
||||||
// Stats is a collection of methods used to interact with stats.
|
|
||||||
Stats *StatsService
|
|
||||||
// Reminders is a collection of methods used to interact with reminders.
|
|
||||||
Reminders *RemindersService
|
|
||||||
// Telemetry is a collection of methods used to interact with telemetry.
|
|
||||||
Telemetry *TelemetryService
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new instance of Client using the configuration from the given Mattermost Client.
|
|
||||||
func New(client4 *model.Client4) (*Client, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
ts := oauth2.StaticTokenSource(
|
|
||||||
&oauth2.Token{AccessToken: client4.AuthToken},
|
|
||||||
)
|
|
||||||
|
|
||||||
return newClient(client4.URL, oauth2.NewClient(ctx, ts))
|
|
||||||
}
|
|
||||||
|
|
||||||
// newClient creates a new instance of Client from the given URL and http.Client.
|
|
||||||
func newClient(mattermostSiteURL string, httpClient *http.Client) (*Client, error) {
|
|
||||||
siteURL, err := url.Parse(mattermostSiteURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &Client{client: httpClient, BaseURL: siteURL, UserAgent: userAgent}
|
|
||||||
c.PlaybookRuns = &PlaybookRunService{c}
|
|
||||||
c.Playbooks = &PlaybooksService{c}
|
|
||||||
c.Settings = &SettingsService{c}
|
|
||||||
c.Actions = &ActionsService{c}
|
|
||||||
c.Stats = &StatsService{c}
|
|
||||||
c.Reminders = &RemindersService{c}
|
|
||||||
c.Telemetry = &TelemetryService{c}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newRequest creates an API request, JSON-encoding any given body parameter.
|
|
||||||
func (c *Client) newRequest(method, endpoint string, body interface{}) (*http.Request, error) {
|
|
||||||
u, err := c.BaseURL.Parse(buildAPIURL(endpoint))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "invalid endpoint %s", endpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf io.ReadWriter
|
|
||||||
if body != nil {
|
|
||||||
buf = &bytes.Buffer{}
|
|
||||||
enc := json.NewEncoder(buf)
|
|
||||||
enc.SetEscapeHTML(false)
|
|
||||||
err = enc.Encode(body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed to encode body %s", body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(method, u.String(), buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed to create http request for url %s", u)
|
|
||||||
}
|
|
||||||
|
|
||||||
if buf != nil {
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
}
|
|
||||||
if c.UserAgent != "" {
|
|
||||||
req.Header.Set("User-Agent", c.UserAgent)
|
|
||||||
}
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildAPIURL constructs the path to the given endpoint.
|
|
||||||
func buildAPIURL(endpoint string) string {
|
|
||||||
return fmt.Sprintf("plugins/%s/api/%s/%s", manifestID, apiVersion, endpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// do sends an API request and returns the API response.
|
|
||||||
//
|
|
||||||
// The API response is JSON decoded and stored in the value pointed to by v, or returned as an
|
|
||||||
// error if an API error has occurred. If v implements the io.Writer
|
|
||||||
// interface, the raw response body will be written to v, without attempting to
|
|
||||||
// first decode it.
|
|
||||||
func (c *Client) do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) {
|
|
||||||
if ctx == nil {
|
|
||||||
return nil, errors.New("context must be non-nil")
|
|
||||||
}
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
|
|
||||||
resp, err := c.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, errors.Wrapf(ctx.Err(), "client err=%s", err.Error())
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
err = checkResponse(resp)
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if v != nil {
|
|
||||||
if w, ok := v.(io.Writer); ok {
|
|
||||||
if _, err = io.Copy(w, resp.Body); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
|
|
||||||
decErr := json.NewDecoder(bytes.NewReader(body)).Decode(v)
|
|
||||||
if decErr == io.EOF {
|
|
||||||
// TODO: Confirm if this happens only on empty bodies. If so, check that first before decoding.
|
|
||||||
decErr = nil // ignore EOF errors caused by empty response body
|
|
||||||
}
|
|
||||||
if decErr != nil {
|
|
||||||
err = decErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
type GraphQLInput struct {
|
|
||||||
Query string `json:"query"`
|
|
||||||
OperationName string `json:"operationName"`
|
|
||||||
Variables map[string]interface{} `json:"variables"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) DoGraphql(ctx context.Context, input *GraphQLInput, v interface{}) error {
|
|
||||||
url := "query"
|
|
||||||
req, err := c.newRequest(http.MethodPost, url, input)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = c.do(ctx, req, v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkResponse checks the API response for an error.
|
|
||||||
//
|
|
||||||
// Any response with a status code outside 2xx is considered an error, and its body inspected for
|
|
||||||
// an optional `Error` property in a JSON struct.
|
|
||||||
func checkResponse(r *http.Response) error {
|
|
||||||
if c := r.StatusCode; http.StatusOK <= c && c <= 299 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
errorResponse := &ErrorResponse{
|
|
||||||
StatusCode: r.StatusCode,
|
|
||||||
Method: r.Request.Method,
|
|
||||||
URL: r.Request.URL.String(),
|
|
||||||
}
|
|
||||||
data, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
errorResponse.Err = fmt.Errorf("failed to read response body: %w", err)
|
|
||||||
}
|
|
||||||
r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
|
|
||||||
|
|
||||||
if data != nil {
|
|
||||||
_ = json.Unmarshal(data, errorResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errorResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
// addOption adds the given parameter as an URL query parameters to s.
|
|
||||||
func addOption(s string, name, value string) (string, error) {
|
|
||||||
u, err := url.Parse(s)
|
|
||||||
if err != nil {
|
|
||||||
return s, errors.Wrapf(err, "failed to parse %s", s)
|
|
||||||
}
|
|
||||||
|
|
||||||
qa := u.Query()
|
|
||||||
qa.Add(name, value)
|
|
||||||
u.RawQuery = qa.Encode()
|
|
||||||
|
|
||||||
return u.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// addOptions adds the parameters in opts as URL query parameters to s. opts
|
|
||||||
// must be a struct whose fields may contain "url" tags.
|
|
||||||
func addOptions(s string, opts interface{}) (string, error) {
|
|
||||||
v := reflect.ValueOf(opts)
|
|
||||||
if v.Kind() == reflect.Ptr && v.IsNil() {
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(s)
|
|
||||||
if err != nil {
|
|
||||||
return s, errors.Wrapf(err, "failed to parse %s", s)
|
|
||||||
}
|
|
||||||
|
|
||||||
qs, err := query.Values(opts)
|
|
||||||
if err != nil {
|
|
||||||
return s, errors.Wrapf(err, "failed to opts %+v", opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append to the existing query parameters.
|
|
||||||
qa := u.Query()
|
|
||||||
for key, values := range qs {
|
|
||||||
for _, value := range values {
|
|
||||||
qa.Add(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
u.RawQuery = qa.Encode()
|
|
||||||
return u.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// addPaginationOptions adds the given pagination parameters as URL query parameters to s.
|
|
||||||
func addPaginationOptions(s string, page, perPage int) (string, error) {
|
|
||||||
u, err := url.Parse(s)
|
|
||||||
if err != nil {
|
|
||||||
return s, errors.Wrapf(err, "failed to parse %s", s)
|
|
||||||
}
|
|
||||||
|
|
||||||
qa := u.Query()
|
|
||||||
qa.Add("page", strconv.Itoa(page))
|
|
||||||
qa.Add("per_page", strconv.Itoa(perPage))
|
|
||||||
u.RawQuery = qa.Encode()
|
|
||||||
|
|
||||||
return u.String(), nil
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
// Package client provides an HTTP client for using the Playbooks API.
|
|
||||||
package client
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Example() {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
client4 := model.NewAPIv4Client("http://localhost:8065")
|
|
||||||
_, _, err := client4.Login(context.Background(), "test@example.com", "testtest")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := client.New(client4)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookRunID := "h4n3h7s1qjf5pkis4dn6cuxgwa"
|
|
||||||
playbookRun, err := c.PlaybookRuns.Get(ctx, playbookRunID)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Playbook Run Name: %s\n", playbookRun.Name)
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrorResponse is an error from an API request.
|
|
||||||
type ErrorResponse struct {
|
|
||||||
// Method is the HTTP verb used in the API request.
|
|
||||||
Method string
|
|
||||||
// URL is the HTTP endpoint used in the API request.
|
|
||||||
URL string
|
|
||||||
// StatusCode is the HTTP status code returned by the API.
|
|
||||||
StatusCode int
|
|
||||||
|
|
||||||
// Err is the error parsed from the API response.
|
|
||||||
Err error `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ErrorResponse) UnmarshalJSON(data []byte) error {
|
|
||||||
type Alias ErrorResponse
|
|
||||||
temp := &struct {
|
|
||||||
Err string `json:"error"`
|
|
||||||
*Alias
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to extract a structured error from the body, otherwise fall back to using
|
|
||||||
// the whole body as the error message.
|
|
||||||
if err := json.Unmarshal(data, &temp); err != nil || temp.Err == "" {
|
|
||||||
e.Err = errors.New(string(data))
|
|
||||||
} else {
|
|
||||||
e.Err = errors.New(temp.Err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unwrap exposes the underlying error of an ErrorResponse.
|
|
||||||
func (e *ErrorResponse) Unwrap() error {
|
|
||||||
return e.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error describes the error from the API request.
|
|
||||||
func (e *ErrorResponse) Error() string {
|
|
||||||
return fmt.Sprintf("%s %s [%d]: %v", e.Method, e.URL, e.StatusCode, e.Err)
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"gopkg.in/guregu/null.v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Playbook represents the planning before a playbook run is initiated.
|
|
||||||
type Playbook struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Public bool `json:"public"`
|
|
||||||
TeamID string `json:"team_id"`
|
|
||||||
CreatePublicPlaybookRun bool `json:"create_public_playbook_run"`
|
|
||||||
CreateAt int64 `json:"create_at"`
|
|
||||||
DeleteAt int64 `json:"delete_at"`
|
|
||||||
NumStages int64 `json:"num_stages"`
|
|
||||||
NumSteps int64 `json:"num_steps"`
|
|
||||||
Checklists []Checklist `json:"checklists"`
|
|
||||||
Members []PlaybookMember `json:"members"`
|
|
||||||
ReminderMessageTemplate string `json:"reminder_message_template"`
|
|
||||||
ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds"`
|
|
||||||
InvitedUserIDs []string `json:"invited_user_ids"`
|
|
||||||
InvitedGroupIDs []string `json:"invited_group_ids"`
|
|
||||||
InviteUsersEnabled bool `json:"invite_users_enabled"`
|
|
||||||
DefaultOwnerID string `json:"default_owner_id"`
|
|
||||||
DefaultOwnerEnabled bool `json:"default_owner_enabled"`
|
|
||||||
BroadcastChannelIDs []string `json:"broadcast_channel_ids"`
|
|
||||||
BroadcastEnabled bool `json:"broadcast_enabled"`
|
|
||||||
WebhookOnCreationURLs []string `json:"webhook_on_creation_urls"`
|
|
||||||
WebhookOnCreationEnabled bool `json:"webhook_on_creation_enabled"`
|
|
||||||
Metrics []PlaybookMetricConfig `json:"metrics"`
|
|
||||||
CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant"`
|
|
||||||
RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant"`
|
|
||||||
ChannelID string `json:"channel_id" export:"channel_id"`
|
|
||||||
ChannelMode ChannelPlaybookMode `json:"channel_mode" export:"channel_mode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlaybookMember struct {
|
|
||||||
UserID string `json:"user_id"`
|
|
||||||
Roles []string `json:"roles"`
|
|
||||||
SchemeRoles []string `json:"scheme_roles"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
MetricTypeDuration = "metric_duration"
|
|
||||||
MetricTypeCurrency = "metric_currency"
|
|
||||||
MetricTypeInteger = "metric_integer"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Checklist represents a checklist in a playbook
|
|
||||||
type Checklist struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Items []ChecklistItem `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChecklistItem represents an item in a checklist
|
|
||||||
type ChecklistItem struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
State string `json:"state"`
|
|
||||||
StateModified int64 `json:"state_modified"`
|
|
||||||
AssigneeID string `json:"assignee_id"`
|
|
||||||
AssigneeModified int64 `json:"assignee_modified"`
|
|
||||||
Command string `json:"command"`
|
|
||||||
CommandLastRun int64 `json:"command_last_run"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
LastSkipped int64 `json:"delete_at"`
|
|
||||||
DueDate int64 `json:"due_date"`
|
|
||||||
TaskActions []TaskAction `json:"task_actions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TaskAction represents a task action in an item
|
|
||||||
type TaskAction struct {
|
|
||||||
Trigger TriggerAction `json:"trigger"`
|
|
||||||
Actions []TriggerAction `json:"actions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TriggerAction represents a trigger or action in a Task Action
|
|
||||||
type TriggerAction struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Payload string `json:"payload"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaybookCreateOptions specifies the parameters for PlaybooksService.Create method.
|
|
||||||
type PlaybookCreateOptions struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
TeamID string `json:"team_id"`
|
|
||||||
Public bool `json:"public"`
|
|
||||||
CreatePublicPlaybookRun bool `json:"create_public_playbook_run"`
|
|
||||||
Checklists []Checklist `json:"checklists"`
|
|
||||||
Members []PlaybookMember `json:"members"`
|
|
||||||
BroadcastChannelID string `json:"broadcast_channel_id"`
|
|
||||||
ReminderMessageTemplate string `json:"reminder_message_template"`
|
|
||||||
ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds"`
|
|
||||||
InvitedUserIDs []string `json:"invited_user_ids"`
|
|
||||||
InvitedGroupIDs []string `json:"invited_group_ids"`
|
|
||||||
InviteUsersEnabled bool `json:"invite_users_enabled"`
|
|
||||||
DefaultOwnerID string `json:"default_owner_id"`
|
|
||||||
DefaultOwnerEnabled bool `json:"default_owner_enabled"`
|
|
||||||
BroadcastChannelIDs []string `json:"broadcast_channel_ids"`
|
|
||||||
BroadcastEnabled bool `json:"broadcast_enabled"`
|
|
||||||
Metrics []PlaybookMetricConfig `json:"metrics"`
|
|
||||||
CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant"`
|
|
||||||
RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant"`
|
|
||||||
ChannelID string `json:"channel_id" export:"channel_id"`
|
|
||||||
ChannelMode ChannelPlaybookMode `json:"channel_mode" export:"channel_mode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlaybookMetricConfig struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
PlaybookID string `json:"playbook_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Target null.Int `json:"target"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaybookListOptions specifies the optional parameters to the
|
|
||||||
// PlaybooksService.List method.
|
|
||||||
type PlaybookListOptions struct {
|
|
||||||
Sort Sort `url:"sort,omitempty"`
|
|
||||||
Direction SortDirection `url:"direction,omitempty"`
|
|
||||||
SearchTeam string `url:"search_term,omitempty"`
|
|
||||||
WithArchived bool `url:"with_archived,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetPlaybooksResults struct {
|
|
||||||
TotalCount int `json:"total_count"`
|
|
||||||
PageCount int `json:"page_count"`
|
|
||||||
HasMore bool `json:"has_more"`
|
|
||||||
Items []Playbook `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlaybookStats struct {
|
|
||||||
RunsInProgress int `json:"runs_in_progress"`
|
|
||||||
ParticipantsActive int `json:"participants_active"`
|
|
||||||
RunsFinishedPrev30Days int `json:"runs_finished_prev_30_days"`
|
|
||||||
RunsFinishedPercentageChange int `json:"runs_finished_percentage_change"`
|
|
||||||
RunsStartedPerWeek []int `json:"runs_started_per_week"`
|
|
||||||
RunsStartedPerWeekTimes [][]int64 `json:"runs_started_per_week_times"`
|
|
||||||
ActiveRunsPerDay []int `json:"active_runs_per_day"`
|
|
||||||
ActiveRunsPerDayTimes [][]int64 `json:"active_runs_per_day_times"`
|
|
||||||
ActiveParticipantsPerDay []int `json:"active_participants_per_day"`
|
|
||||||
ActiveParticipantsPerDayTimes [][]int64 `json:"active_participants_per_day_times"`
|
|
||||||
MetricOverallAverage []null.Int `json:"metric_overall_average"`
|
|
||||||
MetricRollingAverage []null.Int `json:"metric_rolling_average"`
|
|
||||||
MetricRollingAverageChange []null.Int `json:"metric_rolling_average_change"`
|
|
||||||
MetricValueRange [][]int64 `json:"metric_value_range"`
|
|
||||||
MetricRollingValues [][]int64 `json:"metric_rolling_values"`
|
|
||||||
LastXRunNames []string `json:"last_x_run_names"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChannelPlaybookMode int
|
|
||||||
|
|
||||||
const (
|
|
||||||
PlaybookRunCreateNewChannel ChannelPlaybookMode = iota
|
|
||||||
PlaybookRunLinkExistingChannel
|
|
||||||
)
|
|
||||||
|
|
||||||
var channelPlaybookTypes = [...]string{
|
|
||||||
PlaybookRunCreateNewChannel: "create_new_channel",
|
|
||||||
PlaybookRunLinkExistingChannel: "link_existing_channel",
|
|
||||||
}
|
|
||||||
|
|
||||||
// String creates the string version of the TelemetryTrack
|
|
||||||
func (cpm ChannelPlaybookMode) String() string {
|
|
||||||
return channelPlaybookTypes[cpm]
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalText converts a ChannelPlaybookMode to a string for serializers (including JSON)
|
|
||||||
func (cpm ChannelPlaybookMode) MarshalText() ([]byte, error) {
|
|
||||||
return []byte(channelPlaybookTypes[cpm]), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalText parses a ChannelPlaybookMode from text. For deserializers (including JSON)
|
|
||||||
func (cpm *ChannelPlaybookMode) UnmarshalText(text []byte) error {
|
|
||||||
for i, st := range channelPlaybookTypes {
|
|
||||||
if st == string(text) {
|
|
||||||
*cpm = ChannelPlaybookMode(i)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unknown ChannelPlaybookMode: %s", string(text))
|
|
||||||
}
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gopkg.in/guregu/null.v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Me is a constant that refers to the current user, and can be used in various APIs in place of
|
|
||||||
// explicitly specifying the current user's id.
|
|
||||||
const Me = "me"
|
|
||||||
|
|
||||||
// PlaybookRun represents a playbook run.
|
|
||||||
type PlaybookRun struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Summary string `json:"summary"`
|
|
||||||
SummaryModifiedAt int64 `json:"summary_modified_at"`
|
|
||||||
OwnerUserID string `json:"owner_user_id"`
|
|
||||||
ReporterUserID string `json:"reporter_user_id"`
|
|
||||||
TeamID string `json:"team_id"`
|
|
||||||
ChannelID string `json:"channel_id"`
|
|
||||||
CreateAt int64 `json:"create_at"`
|
|
||||||
EndAt int64 `json:"end_at"`
|
|
||||||
DeleteAt int64 `json:"delete_at"`
|
|
||||||
ActiveStage int `json:"active_stage"`
|
|
||||||
ActiveStageTitle string `json:"active_stage_title"`
|
|
||||||
PostID string `json:"post_id"`
|
|
||||||
PlaybookID string `json:"playbook_id"`
|
|
||||||
Checklists []Checklist `json:"checklists"`
|
|
||||||
StatusPosts []StatusPost `json:"status_posts"`
|
|
||||||
CurrentStatus string `json:"current_status"`
|
|
||||||
LastStatusUpdateAt int64 `json:"last_status_update_at"`
|
|
||||||
ReminderPostID string `json:"reminder_post_id"`
|
|
||||||
PreviousReminder time.Duration `json:"previous_reminder"`
|
|
||||||
ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds"`
|
|
||||||
StatusUpdateEnabled bool `json:"status_update_enabled"`
|
|
||||||
BroadcastChannelIDs []string `json:"broadcast_channel_ids"`
|
|
||||||
WebhookOnStatusUpdateURLs []string `json:"webhook_on_status_update_urls"`
|
|
||||||
StatusUpdateBroadcastChannelsEnabled bool `json:"status_update_broadcast_channels_enabled"`
|
|
||||||
StatusUpdateBroadcastWebhooksEnabled bool `json:"status_update_broadcast_webhooks_enabled"`
|
|
||||||
ReminderMessageTemplate string `json:"reminder_message_template"`
|
|
||||||
InvitedUserIDs []string `json:"invited_user_ids"`
|
|
||||||
InvitedGroupIDs []string `json:"invited_group_ids"`
|
|
||||||
TimelineEvents []TimelineEvent `json:"timeline_events"`
|
|
||||||
DefaultOwnerID string `json:"default_owner_id"`
|
|
||||||
WebhookOnCreationURLs []string `json:"webhook_on_creation_urls"`
|
|
||||||
Retrospective string `json:"retrospective"`
|
|
||||||
RetrospectivePublishedAt int64 `json:"retrospective_published_at"`
|
|
||||||
RetrospectiveWasCanceled bool `json:"retrospective_was_canceled"`
|
|
||||||
RetrospectiveReminderIntervalSeconds int64 `json:"retrospective_reminder_interval_seconds"`
|
|
||||||
RetrospectiveEnabled bool `json:"retrospective_enabled"`
|
|
||||||
MessageOnJoin string `json:"message_on_join"`
|
|
||||||
ParticipantIDs []string `json:"participant_ids"`
|
|
||||||
CategoryName string `json:"category_name"`
|
|
||||||
MetricsData []RunMetricData `json:"metrics_data"`
|
|
||||||
CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant"`
|
|
||||||
RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatusPost is information added to the playbook run when selecting from the db and sent to the
|
|
||||||
// client; it is not saved to the db.
|
|
||||||
type StatusPost struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
CreateAt int64 `json:"create_at"`
|
|
||||||
DeleteAt int64 `json:"delete_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatusPostComplete is the complete status update (post)
|
|
||||||
// it's similar to StatusPost but with extended info.
|
|
||||||
type StatusPostComplete struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
CreateAt int64 `json:"create_at"`
|
|
||||||
UpdateAt int64 `json:"update_at"`
|
|
||||||
DeleteAt int64 `json:"delete_at"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
AuthorUserName string `json:"author_user_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata tracks ancillary metadata about a playbook run.
|
|
||||||
type Metadata struct {
|
|
||||||
ChannelName string `json:"channel_name"`
|
|
||||||
ChannelDisplayName string `json:"channel_display_name"`
|
|
||||||
TeamName string `json:"team_name"`
|
|
||||||
NumParticipants int64 `json:"num_participants"`
|
|
||||||
TotalPosts int64 `json:"total_posts"`
|
|
||||||
Followers []string `json:"followers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimelineEventType describes a type of timeline event.
|
|
||||||
type TimelineEventType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
PlaybookRunCreated TimelineEventType = "incident_created"
|
|
||||||
TaskStateModified TimelineEventType = "task_state_modified"
|
|
||||||
StatusUpdated TimelineEventType = "status_updated"
|
|
||||||
StatusUpdateRequested TimelineEventType = "status_update_requested"
|
|
||||||
OwnerChanged TimelineEventType = "owner_changed"
|
|
||||||
AssigneeChanged TimelineEventType = "assignee_changed"
|
|
||||||
RanSlashCommand TimelineEventType = "ran_slash_command"
|
|
||||||
EventFromPost TimelineEventType = "event_from_post"
|
|
||||||
UserJoinedLeft TimelineEventType = "user_joined_left"
|
|
||||||
PublishedRetrospective TimelineEventType = "published_retrospective"
|
|
||||||
CanceledRetrospective TimelineEventType = "canceled_retrospective"
|
|
||||||
RunFinished TimelineEventType = "run_finished"
|
|
||||||
RunRestored TimelineEventType = "run_restored"
|
|
||||||
StatusUpdatesEnabled TimelineEventType = "status_updates_enabled"
|
|
||||||
StatusUpdatesDisabled TimelineEventType = "status_updates_disabled"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TimelineEvent represents an event recorded to a playbook run's timeline.
|
|
||||||
type TimelineEvent struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
PlaybookRunID string `json:"playbook_run"`
|
|
||||||
CreateAt int64 `json:"create_at"`
|
|
||||||
DeleteAt int64 `json:"delete_at"`
|
|
||||||
EventAt int64 `json:"event_at"`
|
|
||||||
EventType TimelineEventType `json:"event_type"`
|
|
||||||
Summary string `json:"summary"`
|
|
||||||
Details string `json:"details"`
|
|
||||||
PostID string `json:"post_id"`
|
|
||||||
SubjectUserID string `json:"subject_user_id"`
|
|
||||||
CreatorUserID string `json:"creator_user_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaybookRunCreateOptions specifies the parameters for PlaybookRunService.Create method.
|
|
||||||
type PlaybookRunCreateOptions struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
OwnerUserID string `json:"owner_user_id"`
|
|
||||||
TeamID string `json:"team_id"`
|
|
||||||
ChannelID string `json:"channel_id"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
PostID string `json:"post_id"`
|
|
||||||
PlaybookID string `json:"playbook_id"`
|
|
||||||
CreatePublicRun *bool `json:"create_public_run"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunAction represents the run action settings. Frontend passes this struct to update settings.
|
|
||||||
type RunAction struct {
|
|
||||||
BroadcastChannelIDs []string `json:"broadcast_channel_ids"`
|
|
||||||
WebhookOnStatusUpdateURLs []string `json:"webhook_on_status_update_urls"`
|
|
||||||
|
|
||||||
StatusUpdateBroadcastChannelsEnabled bool `json:"status_update_broadcast_channels_enabled"`
|
|
||||||
StatusUpdateBroadcastWebhooksEnabled bool `json:"status_update_broadcast_webhooks_enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RetrospectiveUpdate represents the run retrospective info
|
|
||||||
type RetrospectiveUpdate struct {
|
|
||||||
Text string `json:"retrospective"`
|
|
||||||
Metrics []RunMetricData `json:"metrics"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort enumerates the available fields we can sort on.
|
|
||||||
type Sort string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// SortByCreateAt sorts by the "create_at" field. It is the default.
|
|
||||||
SortByCreateAt Sort = "create_at"
|
|
||||||
|
|
||||||
// SortByID sorts by the "id" field.
|
|
||||||
SortByID Sort = "id"
|
|
||||||
|
|
||||||
// SortByName sorts by the "name" field.
|
|
||||||
SortByName Sort = "name"
|
|
||||||
|
|
||||||
// SortByOwnerUserID sorts by the "owner_user_id" field.
|
|
||||||
SortByOwnerUserID Sort = "owner_user_id"
|
|
||||||
|
|
||||||
// SortByTeamID sorts by the "team_id" field.
|
|
||||||
SortByTeamID Sort = "team_id"
|
|
||||||
|
|
||||||
// SortByEndAt sorts by the "end_at" field.
|
|
||||||
SortByEndAt Sort = "end_at"
|
|
||||||
|
|
||||||
// SortBySteps sorts playbooks by the number of steps in the playbook.
|
|
||||||
SortBySteps Sort = "steps"
|
|
||||||
|
|
||||||
// SortByStages sorts playbooks by the number of stages in the playbook.
|
|
||||||
SortByStages Sort = "stages"
|
|
||||||
|
|
||||||
// SortByTitle sorts by the "title" field.
|
|
||||||
SortByTitle Sort = "title"
|
|
||||||
|
|
||||||
// SortByRuns sorts by the number of times a playbook has been run.
|
|
||||||
SortByRuns Sort = "runs"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SortDirection determines whether results are sorted ascending or descending.
|
|
||||||
type SortDirection string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Desc sorts the results in descending order.
|
|
||||||
SortDesc SortDirection = "desc"
|
|
||||||
|
|
||||||
// Asc sorts the results in ascending order.
|
|
||||||
SortAsc SortDirection = "asc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PlaybookRunListOptions specifies the optional parameters to the
|
|
||||||
// PlaybookRunService.List method.
|
|
||||||
type PlaybookRunListOptions struct {
|
|
||||||
// TeamID filters playbook runs to those in the given team.
|
|
||||||
TeamID string `url:"team_id,omitempty"`
|
|
||||||
|
|
||||||
Sort Sort `url:"sort,omitempty"`
|
|
||||||
Direction SortDirection `url:"direction,omitempty"`
|
|
||||||
|
|
||||||
// Statuses filters by InProgress or Ended; defaults to All when no status specified.
|
|
||||||
Statuses []Status `url:"statuses,omitempty"`
|
|
||||||
|
|
||||||
// OwnerID filters by owner's Mattermost user ID. Defaults to blank (no filter). Specify "me" for current user.
|
|
||||||
OwnerID string `url:"owner_user_id,omitempty"`
|
|
||||||
|
|
||||||
// ParticipantID filters playbook runs that have this user as a participant. Defaults to blank (no filter). Specify "me" for current user.
|
|
||||||
ParticipantID string `url:"participant_id,omitempty"`
|
|
||||||
|
|
||||||
// ParticipantOrFollowerID filters playbook runs that have this user as member or as follower. Defaults to blank (no filter). Specify "me" for current user.
|
|
||||||
ParticipantOrFollowerID string `url:"participant_or_follower,omitempty"`
|
|
||||||
|
|
||||||
// SearchTerm returns results of the search term and respecting the other header filter options.
|
|
||||||
// The search term acts as a filter and respects the Sort and Direction fields (i.e., results are
|
|
||||||
// not returned in relevance order).
|
|
||||||
SearchTerm string `url:"search_term,omitempty"`
|
|
||||||
|
|
||||||
// PlaybookID filters playbook runs that are derived from this playbook id.
|
|
||||||
// Defaults to blank (no filter).
|
|
||||||
PlaybookID string `url:"playbook_id,omitempty"`
|
|
||||||
|
|
||||||
// ActiveGTE filters playbook runs that were active after (or equal) to the unix time given (in millis).
|
|
||||||
// A value of 0 means the filter is ignored (which is the default).
|
|
||||||
ActiveGTE int64 `url:"active_gte,omitempty"`
|
|
||||||
|
|
||||||
// ActiveLT filters playbook runs that were active before the unix time given (in millis).
|
|
||||||
// A value of 0 means the filter is ignored (which is the default).
|
|
||||||
ActiveLT int64 `url:"active_lt,omitempty"`
|
|
||||||
|
|
||||||
// StartedGTE filters playbook runs that were started after (or equal) to the unix time given (in millis).
|
|
||||||
// A value of 0 means the filter is ignored (which is the default).
|
|
||||||
StartedGTE int64 `url:"started_gte,omitempty"`
|
|
||||||
|
|
||||||
// StartedLT filters playbook runs that were started before the unix time given (in millis).
|
|
||||||
// A value of 0 means the filter is ignored (which is the default).
|
|
||||||
StartedLT int64 `url:"started_lt,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaybookRunList contains the paginated result.
|
|
||||||
type PlaybookRunList struct {
|
|
||||||
TotalCount int `json:"total_count"`
|
|
||||||
PageCount int `json:"page_count"`
|
|
||||||
HasMore bool `json:"has_more"`
|
|
||||||
Items []*PlaybookRun
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status is the type used to specify the activity status of the playbook run.
|
|
||||||
type Status string
|
|
||||||
|
|
||||||
const (
|
|
||||||
StatusInProgress Status = "InProgress"
|
|
||||||
StatusFinished Status = "Finished"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GetPlaybookRunsResults struct {
|
|
||||||
TotalCount int `json:"total_count"`
|
|
||||||
PageCount int `json:"page_count"`
|
|
||||||
HasMore bool `json:"has_more"`
|
|
||||||
Items []PlaybookRun `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatusUpdateOptions are the fields required to update a playbook run's status
|
|
||||||
type StatusUpdateOptions struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
Reminder time.Duration `json:"reminder"`
|
|
||||||
FinishRun bool `json:"finish_run"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RunMetricData struct {
|
|
||||||
MetricConfigID string `json:"metric_config_id"`
|
|
||||||
Value null.Int `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OwnerInfo holds the summary information of a owner.
|
|
||||||
type OwnerInfo struct {
|
|
||||||
UserID string `json:"user_id"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
FirstName string `json:"first_name"`
|
|
||||||
LastName string `json:"last_name"`
|
|
||||||
Nickname string `json:"nickname"`
|
|
||||||
}
|
|
||||||
@@ -1,375 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PlaybookRunService handles communication with the playbook run related
|
|
||||||
// methods of the Playbooks API.
|
|
||||||
type PlaybookRunService struct {
|
|
||||||
client *Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a playbook run.
|
|
||||||
func (s *PlaybookRunService) Get(ctx context.Context, playbookRunID string) (*PlaybookRun, error) {
|
|
||||||
playbookRunURL := fmt.Sprintf("runs/%s", playbookRunID)
|
|
||||||
req, err := s.client.newRequest(http.MethodGet, playbookRunURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookRun := new(PlaybookRun)
|
|
||||||
resp, err := s.client.do(ctx, req, playbookRun)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
return playbookRun, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByChannelID gets a playbook run by ChannelID.
|
|
||||||
func (s *PlaybookRunService) GetByChannelID(ctx context.Context, channelID string) (*PlaybookRun, error) {
|
|
||||||
channelURL := fmt.Sprintf("runs/channel/%s", channelID)
|
|
||||||
req, err := s.client.newRequest(http.MethodGet, channelURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookRun := new(PlaybookRun)
|
|
||||||
resp, err := s.client.do(ctx, req, playbookRun)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
return playbookRun, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a playbook run's metadata.
|
|
||||||
func (s *PlaybookRunService) GetMetadata(ctx context.Context, playbookRunID string) (*Metadata, error) {
|
|
||||||
playbookRunURL := fmt.Sprintf("runs/%s/metadata", playbookRunID)
|
|
||||||
req, err := s.client.newRequest(http.MethodGet, playbookRunURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookRun := new(Metadata)
|
|
||||||
resp, err := s.client.do(ctx, req, playbookRun)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
return playbookRun, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all playbook status updates.
|
|
||||||
func (s *PlaybookRunService) GetStatusUpdates(ctx context.Context, playbookRunID string) ([]StatusPostComplete, error) {
|
|
||||||
playbookRunURL := fmt.Sprintf("runs/%s/status-updates", playbookRunID)
|
|
||||||
req, err := s.client.newRequest(http.MethodGet, playbookRunURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var statusUpdates []StatusPostComplete
|
|
||||||
resp, err := s.client.do(ctx, req, &statusUpdates)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
return statusUpdates, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List the playbook runs.
|
|
||||||
func (s *PlaybookRunService) List(ctx context.Context, page, perPage int, opts PlaybookRunListOptions) (*GetPlaybookRunsResults, error) {
|
|
||||||
playbookRunURL := "runs"
|
|
||||||
playbookRunURL, err := addOptions(playbookRunURL, opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to build options: %w", err)
|
|
||||||
}
|
|
||||||
playbookRunURL, err = addPaginationOptions(playbookRunURL, page, perPage)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to build pagination options: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := s.client.newRequest(http.MethodGet, playbookRunURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to build request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := &GetPlaybookRunsResults{}
|
|
||||||
resp, err := s.client.do(ctx, req, result)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to execute request: %w", err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a playbook run.
|
|
||||||
func (s *PlaybookRunService) Create(ctx context.Context, opts PlaybookRunCreateOptions) (*PlaybookRun, error) {
|
|
||||||
playbookRunURL := "runs"
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, playbookRunURL, opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookRun := new(PlaybookRun)
|
|
||||||
resp, err := s.client.do(ctx, req, playbookRun)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusCreated {
|
|
||||||
return nil, fmt.Errorf("expected status code %d", http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
return playbookRun, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunService) UpdateStatus(ctx context.Context, playbookRunID string, message string, reminderInSeconds int64) error {
|
|
||||||
updateURL := fmt.Sprintf("runs/%s/status", playbookRunID)
|
|
||||||
opts := StatusUpdateOptions{
|
|
||||||
Message: message,
|
|
||||||
Reminder: time.Duration(reminderInSeconds),
|
|
||||||
}
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, updateURL, opts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := s.client.do(ctx, req, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("expected status code %d", http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunService) RequestUpdate(ctx context.Context, playbookRunID, userID string) error {
|
|
||||||
requestURL := fmt.Sprintf("runs/%s/request-update", playbookRunID)
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, requestURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := s.client.do(ctx, req, nil)
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("expected status code %d", http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunService) Finish(ctx context.Context, playbookRunID string) error {
|
|
||||||
finishURL := fmt.Sprintf("runs/%s/finish", playbookRunID)
|
|
||||||
req, err := s.client.newRequest(http.MethodPut, finishURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunService) CreateChecklist(ctx context.Context, playbookRunID string, checklist Checklist) error {
|
|
||||||
createURL := fmt.Sprintf("runs/%s/checklists", playbookRunID)
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, createURL, checklist)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunService) RemoveChecklist(ctx context.Context, playbookRunID string, checklistNumber int) error {
|
|
||||||
createURL := fmt.Sprintf("runs/%s/checklists/%d", playbookRunID, checklistNumber)
|
|
||||||
req, err := s.client.newRequest(http.MethodDelete, createURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunService) RenameChecklist(ctx context.Context, playbookRunID string, checklistNumber int, newTitle string) error {
|
|
||||||
createURL := fmt.Sprintf("runs/%s/checklists/%d/rename", playbookRunID, checklistNumber)
|
|
||||||
req, err := s.client.newRequest(http.MethodPut, createURL, struct{ Title string }{newTitle})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunService) AddChecklistItem(ctx context.Context, playbookRunID string, checklistNumber int, checklistItem ChecklistItem) error {
|
|
||||||
addURL := fmt.Sprintf("runs/%s/checklists/%d/add", playbookRunID, checklistNumber)
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, addURL, checklistItem)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunService) MoveChecklist(ctx context.Context, playbookRunID string, sourceChecklistIdx, destChecklistIdx int) error {
|
|
||||||
createURL := fmt.Sprintf("runs/%s/checklists/move", playbookRunID)
|
|
||||||
body := struct {
|
|
||||||
SourceChecklistIdx int `json:"source_checklist_idx"`
|
|
||||||
DestChecklistIdx int `json:"dest_checklist_idx"`
|
|
||||||
}{sourceChecklistIdx, destChecklistIdx}
|
|
||||||
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, createURL, body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunService) MoveChecklistItem(ctx context.Context, playbookRunID string, sourceChecklistIdx, sourceItemIdx, destChecklistIdx, destItemIdx int) error {
|
|
||||||
createURL := fmt.Sprintf("runs/%s/checklists/move-item", playbookRunID)
|
|
||||||
body := struct {
|
|
||||||
SourceChecklistIdx int `json:"source_checklist_idx"`
|
|
||||||
SourceItemIdx int `json:"source_item_idx"`
|
|
||||||
DestChecklistIdx int `json:"dest_checklist_idx"`
|
|
||||||
DestItemIdx int `json:"dest_item_idx"`
|
|
||||||
}{sourceChecklistIdx, sourceItemIdx, destChecklistIdx, destItemIdx}
|
|
||||||
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, createURL, body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateRetrospective updates the run's retrospective info
|
|
||||||
func (s *PlaybookRunService) UpdateRetrospective(ctx context.Context, playbookRunID, userID string, retroUpdate RetrospectiveUpdate) error {
|
|
||||||
createURL := fmt.Sprintf("runs/%s/retrospective", playbookRunID)
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, createURL, retroUpdate)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := s.client.do(ctx, req, nil)
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("expected status code %d", http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublishRetrospective publishes the run's retrospective
|
|
||||||
func (s *PlaybookRunService) PublishRetrospective(ctx context.Context, playbookRunID, userID string, retroUpdate RetrospectiveUpdate) error {
|
|
||||||
createURL := fmt.Sprintf("runs/%s/retrospective/publish", playbookRunID)
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, createURL, retroUpdate)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := s.client.do(ctx, req, nil)
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("expected status code %d", http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunService) SetItemAssignee(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int, assigneeID string) error {
|
|
||||||
createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/assignee", playbookRunID, checklistIdx, itemIdx)
|
|
||||||
body := struct {
|
|
||||||
AssigneeID string `json:"assignee_id"`
|
|
||||||
}{assigneeID}
|
|
||||||
|
|
||||||
req, err := s.client.newRequest(http.MethodPut, createURL, body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunService) SetItemCommand(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int, newCommand string) error {
|
|
||||||
createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/command", playbookRunID, checklistIdx, itemIdx)
|
|
||||||
body := struct {
|
|
||||||
Command string `json:"command"`
|
|
||||||
}{newCommand}
|
|
||||||
|
|
||||||
req, err := s.client.newRequest(http.MethodPut, createURL, body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunService) RunItemCommand(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int) error {
|
|
||||||
createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/run", playbookRunID, checklistIdx, itemIdx)
|
|
||||||
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, createURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunService) SetItemDueDate(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int, duedate int64) error {
|
|
||||||
createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/duedate", playbookRunID, checklistIdx, itemIdx)
|
|
||||||
body := struct {
|
|
||||||
DueDate int64 `json:"due_date"`
|
|
||||||
}{duedate}
|
|
||||||
|
|
||||||
req, err := s.client.newRequest(http.MethodPut, createURL, body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a playbook run.
|
|
||||||
func (s *PlaybookRunService) GetOwners(ctx context.Context) ([]OwnerInfo, error) {
|
|
||||||
req, err := s.client.newRequest(http.MethodGet, "runs/owners", nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
owners := make([]OwnerInfo, 0)
|
|
||||||
resp, err := s.client.do(ctx, req, &owners)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
return owners, nil
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExamplePlaybookRunService_Get() {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
client4 := model.NewAPIv4Client("http://localhost:8065")
|
|
||||||
client4.Login(context.Background(), "test@example.com", "testtest")
|
|
||||||
|
|
||||||
c, err := client.New(client4)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookRunID := "h4n3h7s1qjf5pkis4dn6cuxgwa"
|
|
||||||
playbookRun, err := c.PlaybookRuns.Get(ctx, playbookRunID)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Playbook Run Name: %s\n", playbookRun.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExamplePlaybookRunService_List() {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
client4 := model.NewAPIv4Client("http://localhost:8065")
|
|
||||||
_, _, err := client4.Login(context.Background(), "test@example.com", "testtest")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
teams, _, err := client4.GetAllTeams(context.Background(), "", 0, 1)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
if len(teams) == 0 {
|
|
||||||
log.Fatal("no teams for this user")
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := client.New(client4)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var playbookRuns []client.PlaybookRun
|
|
||||||
for page := 0; ; page++ {
|
|
||||||
result, err := c.PlaybookRuns.List(ctx, page, 100, client.PlaybookRunListOptions{
|
|
||||||
TeamID: teams[0].Id,
|
|
||||||
Sort: client.SortByCreateAt,
|
|
||||||
Direction: client.SortDesc,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookRuns = append(playbookRuns, result.Items...)
|
|
||||||
if !result.HasMore {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, playbookRun := range playbookRuns {
|
|
||||||
fmt.Printf("Playbook Run Name: %s\n", playbookRun.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PlaybooksService handles communication with the playbook related
|
|
||||||
// methods of the Playbook API.
|
|
||||||
type PlaybooksService struct {
|
|
||||||
client *Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a playbook.
|
|
||||||
func (s *PlaybooksService) Get(ctx context.Context, playbookID string) (*Playbook, error) {
|
|
||||||
playbookURL := fmt.Sprintf("playbooks/%s", playbookID)
|
|
||||||
req, err := s.client.newRequest(http.MethodGet, playbookURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
playbook := new(Playbook)
|
|
||||||
resp, err := s.client.do(ctx, req, playbook)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
return playbook, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List the playbooks.
|
|
||||||
func (s *PlaybooksService) List(ctx context.Context, teamId string, page, perPage int, opts PlaybookListOptions) (*GetPlaybooksResults, error) {
|
|
||||||
playbookURL := "playbooks"
|
|
||||||
playbookURL, err := addOption(playbookURL, "team_id", teamId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to build options: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookURL, err = addPaginationOptions(playbookURL, page, perPage)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to build pagination options: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookURL, err = addOptions(playbookURL, opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to build options: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := s.client.newRequest(http.MethodGet, playbookURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to build request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := &GetPlaybooksResults{}
|
|
||||||
resp, err := s.client.do(ctx, req, result)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to execute request: %w", err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a playbook. Returns the id of the newly created playbook
|
|
||||||
func (s *PlaybooksService) Create(ctx context.Context, opts PlaybookCreateOptions) (string, error) {
|
|
||||||
// For ease of use set the default if not specificed so it doesn't just error
|
|
||||||
if opts.ReminderTimerDefaultSeconds == 0 {
|
|
||||||
opts.ReminderTimerDefaultSeconds = 86400
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookURL := "playbooks"
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, playbookURL, opts)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
resp, err := s.client.do(ctx, req, &result)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusCreated {
|
|
||||||
return "", fmt.Errorf("expected status code %d", http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybooksService) Update(ctx context.Context, playbook Playbook) error {
|
|
||||||
updateURL := fmt.Sprintf("playbooks/%s", playbook.ID)
|
|
||||||
req, err := s.client.newRequest(http.MethodPut, updateURL, playbook)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybooksService) Archive(ctx context.Context, playbookID string) error {
|
|
||||||
updateURL := fmt.Sprintf("playbooks/%s", playbookID)
|
|
||||||
req, err := s.client.newRequest(http.MethodDelete, updateURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybooksService) Export(ctx context.Context, playbookID string) ([]byte, error) {
|
|
||||||
url := fmt.Sprintf("playbooks/%s/export", playbookID)
|
|
||||||
req, err := s.client.newRequest(http.MethodGet, url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := s.client.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
result, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("expected status code %d", http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duplicate a playbook. Returns the id of the newly created playbook
|
|
||||||
func (s *PlaybooksService) Duplicate(ctx context.Context, playbookID string) (string, error) {
|
|
||||||
url := fmt.Sprintf("playbooks/%s/duplicate", playbookID)
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
resp, err := s.client.do(ctx, req, &result)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusCreated {
|
|
||||||
return "", fmt.Errorf("expected status code %d", http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Imports a playbook. Returns the id of the newly created playbook
|
|
||||||
func (s *PlaybooksService) Import(ctx context.Context, toImport []byte, team string) (string, error) {
|
|
||||||
url := "playbooks/import?team_id=" + team
|
|
||||||
u, err := s.client.BaseURL.Parse(buildAPIURL(url))
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrapf(err, "invalid endpoint %s", url)
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(toImport))
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrapf(err, "failed to create http request for import")
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
resp, err := s.client.do(ctx, req, &result)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusCreated {
|
|
||||||
return "", fmt.Errorf("expected status code %d", http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybooksService) Stats(ctx context.Context, playbookID string) (*PlaybookStats, error) {
|
|
||||||
playbookStatsURL := fmt.Sprintf("stats/playbook?playbook_id=%s", playbookID)
|
|
||||||
req, err := s.client.newRequest(http.MethodGet, playbookStatsURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
stats := new(PlaybookStats)
|
|
||||||
resp, err := s.client.do(ctx, req, stats)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
return stats, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybooksService) AutoFollow(ctx context.Context, playbookID string, userID string) error {
|
|
||||||
followsURL := fmt.Sprintf("playbooks/%s/autofollows/%s", playbookID, userID)
|
|
||||||
req, err := s.client.newRequest(http.MethodPut, followsURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybooksService) AutoUnfollow(ctx context.Context, playbookID string, userID string) error {
|
|
||||||
followsURL := fmt.Sprintf("playbooks/%s/autofollows/%s", playbookID, userID)
|
|
||||||
req, err := s.client.newRequest(http.MethodDelete, followsURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybooksService) GetAutoFollows(ctx context.Context, playbookID string) ([]string, error) {
|
|
||||||
autofollowsURL := fmt.Sprintf("playbooks/%s/autofollows", playbookID)
|
|
||||||
req, err := s.client.newRequest(http.MethodGet, autofollowsURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var followers []string
|
|
||||||
resp, err := s.client.do(ctx, req, &followers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
return followers, nil
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExamplePlaybooksService_Get() {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
client4 := model.NewAPIv4Client("http://localhost:8065")
|
|
||||||
client4.Login(context.Background(), "test@example.com", "testtest")
|
|
||||||
|
|
||||||
c, err := client.New(client4)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookID := "h4n3h7s1qjf5pkis4dn6cuxgwa"
|
|
||||||
playbook, err := c.Playbooks.Get(ctx, playbookID)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Playbook Name: %s\n", playbook.Title)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExamplePlaybooksService_List() {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
client4 := model.NewAPIv4Client("http://localhost:8065")
|
|
||||||
_, _, err := client4.Login(context.Background(), "test@example.com", "testtest")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
teams, _, err := client4.GetAllTeams(context.Background(), "", 0, 1)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
if len(teams) == 0 {
|
|
||||||
log.Fatal("no teams for this user")
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := client.New(client4)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var playbooks []client.Playbook
|
|
||||||
for page := 0; ; page++ {
|
|
||||||
result, err := c.Playbooks.List(ctx, teams[0].Id, page, 100, client.PlaybookListOptions{
|
|
||||||
Sort: client.SortByCreateAt,
|
|
||||||
Direction: client.SortDesc,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
playbooks = append(playbooks, result.Items...)
|
|
||||||
if !result.HasMore {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, playbook := range playbooks {
|
|
||||||
fmt.Printf("Playbook Name: %s\n", playbook.Title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
type ReminderResetPayload struct {
|
|
||||||
NewReminderSeconds int `json:"new_reminder_seconds"`
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RemindersService struct {
|
|
||||||
client *Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RemindersService) Reset(ctx context.Context, playbookRunID string, payload ReminderResetPayload) error {
|
|
||||||
resetURL := fmt.Sprintf("runs/%s/reminder", playbookRunID)
|
|
||||||
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, resetURL, payload)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := s.client.do(ctx, req, ioutil.Discard)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusNoContent {
|
|
||||||
return fmt.Errorf("expected status code %d", http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GlobalSettings struct {
|
|
||||||
// EnableExperimentalFeatures is a read-only field set to true when experimental features
|
|
||||||
// are enabled. Changing this field requires access to the system console plugin
|
|
||||||
// configuration.
|
|
||||||
EnableExperimentalFeatures bool `json:"enable_experimental_features"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SettingsService handles communication with the settings related methods.
|
|
||||||
type SettingsService struct {
|
|
||||||
client *Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the configured settings.
|
|
||||||
func (s *SettingsService) Get(ctx context.Context) (*GlobalSettings, error) {
|
|
||||||
settingsURL := "settings"
|
|
||||||
req, err := s.client.newRequest(http.MethodGet, settingsURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
settings := new(GlobalSettings)
|
|
||||||
resp, err := s.client.do(ctx, req, settings)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
return settings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the configured settings.
|
|
||||||
func (s *SettingsService) Update(ctx context.Context, settings GlobalSettings) error {
|
|
||||||
settingsURL := "settings"
|
|
||||||
req, err := s.client.newRequest(http.MethodPut, settingsURL, settings)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.client.do(ctx, req, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StatsService handles communication with the stats related methods.
|
|
||||||
type StatsService struct {
|
|
||||||
client *Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaybookSiteStats holds the data that we want to expose in system console
|
|
||||||
type PlaybookSiteStats struct {
|
|
||||||
TotalPlaybooks int `json:"total_playbooks"`
|
|
||||||
TotalPlaybookRuns int `json:"total_playbook_runs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the stats that should be displayed in system console.
|
|
||||||
func (s *StatsService) GetSiteStats(ctx context.Context) (*PlaybookSiteStats, error) {
|
|
||||||
statsURL := "stats/site"
|
|
||||||
req, err := s.client.newRequest(http.MethodGet, statsURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
stats := new(PlaybookSiteStats)
|
|
||||||
resp, err := s.client.do(ctx, req, stats)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
return stats, nil
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TelemetryService struct {
|
|
||||||
client *Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TelemetryService) CreateEvent(ctx context.Context, name string, eventType string, properties map[string]interface{}) error {
|
|
||||||
|
|
||||||
payload := struct {
|
|
||||||
Type string
|
|
||||||
Name string
|
|
||||||
Properties map[string]interface{}
|
|
||||||
}{
|
|
||||||
Type: eventType,
|
|
||||||
Name: name,
|
|
||||||
Properties: properties,
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := s.client.newRequest(http.MethodPost, "telemetry", payload)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := s.client.do(ctx, req, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusNoContent {
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fmt.Errorf("expected status code %d, got %d: %s", http.StatusNoContent, resp.StatusCode, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
var BuildAPIURL = buildAPIURL
|
|
||||||
var NewClient = newClient
|
|
||||||
@@ -1,497 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package product
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
mm_model "github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/channels/app/request"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
)
|
|
||||||
|
|
||||||
// normalizeAppError returns a truly nil error if appErr is nil
|
|
||||||
// See https://golang.org/doc/faq#nil_error for more details.
|
|
||||||
func normalizeAppErr(appErr *mm_model.AppError) error {
|
|
||||||
if appErr == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if appErr.StatusCode == http.StatusNotFound {
|
|
||||||
return app.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return appErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// serviceAPIAdapter is an adapter that flattens the APIs provided by suite services so they can
|
|
||||||
// be used as per the Plugin API.
|
|
||||||
// Note: when supporting a plugin build is no longer needed this adapter may be removed as the Boards app
|
|
||||||
// can be modified to use the services in modular fashion.
|
|
||||||
type serviceAPIAdapter struct {
|
|
||||||
api *playbooksProduct
|
|
||||||
ctx *request.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func newServiceAPIAdapter(api *playbooksProduct) *serviceAPIAdapter {
|
|
||||||
return &serviceAPIAdapter{
|
|
||||||
api: api,
|
|
||||||
ctx: request.EmptyContext(api.logger),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Channels service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error) {
|
|
||||||
channel, appErr := a.api.channelService.GetDirectChannel(userID1, userID2)
|
|
||||||
return channel, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetChannelByID(channelID string) (*mm_model.Channel, error) {
|
|
||||||
channel, appErr := a.api.channelService.GetChannelByID(channelID)
|
|
||||||
return channel, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error) {
|
|
||||||
member, appErr := a.api.channelService.GetChannelMember(channelID, userID)
|
|
||||||
return member, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool) (mm_model.ChannelList, error) {
|
|
||||||
opts := &mm_model.ChannelSearchOpts{
|
|
||||||
IncludeDeleted: includeDeleted,
|
|
||||||
}
|
|
||||||
channels, appErr := a.api.channelService.GetChannelsForTeamForUser(teamID, userID, opts)
|
|
||||||
return channels, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetChannelSidebarCategories(userID, teamID string) (*mm_model.OrderedSidebarCategories, error) {
|
|
||||||
categories, appErr := a.api.channelService.GetChannelSidebarCategories(userID, teamID)
|
|
||||||
return categories, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetChannelMembers(channelID string, page, perPage int) (mm_model.ChannelMembers, error) {
|
|
||||||
channelMembers, appErr := a.api.channelService.GetChannelMembers(channelID, page, perPage)
|
|
||||||
return channelMembers, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) {
|
|
||||||
channels, appErr := a.api.channelService.CreateChannelSidebarCategory(userID, teamID, newCategory)
|
|
||||||
return channels, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, error) {
|
|
||||||
channels, appErr := a.api.channelService.UpdateChannelSidebarCategories(userID, teamID, categories)
|
|
||||||
return channels, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) CreateChannel(channel *mm_model.Channel) error {
|
|
||||||
_, appErr := a.api.channelService.CreateChannel(channel)
|
|
||||||
return normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) AddMemberToChannel(channelID, userID string) (*mm_model.ChannelMember, error) {
|
|
||||||
channelMember, appErr := a.api.channelService.AddChannelMember(channelID, userID)
|
|
||||||
return channelMember, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) AddUserToChannel(channelID, userID, asUserID string) (*mm_model.ChannelMember, error) {
|
|
||||||
channel, appErr := a.api.channelService.AddUserToChannel(channelID, userID, asUserID)
|
|
||||||
return channel, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) UpdateChannelMemberRoles(channelID, userID, newRoles string) (*mm_model.ChannelMember, error) {
|
|
||||||
channelMember, appErr := a.api.channelService.UpdateChannelMemberRoles(channelID, userID, newRoles)
|
|
||||||
return channelMember, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
func (a *serviceAPIAdapter) DeleteChannelMember(channelID, userID string) error {
|
|
||||||
appErr := a.api.channelService.DeleteChannelMember(channelID, userID)
|
|
||||||
return normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) AddChannelMember(channelID, userID string) (*mm_model.ChannelMember, error) {
|
|
||||||
channelMember, appErr := a.api.channelService.AddChannelMember(channelID, userID)
|
|
||||||
return channelMember, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error) {
|
|
||||||
channel, appErr := a.api.channelService.GetDirectChannelOrCreate(userID1, userID2)
|
|
||||||
return channel, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Post service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) CreatePost(post *mm_model.Post) (*mm_model.Post, error) {
|
|
||||||
createdPost, appErr := a.api.postService.CreatePost(a.ctx, post)
|
|
||||||
if appErr != nil {
|
|
||||||
return nil, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := createdPost.ShallowCopy(post)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return post, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetPostsByIds(postIDs []string) ([]*mm_model.Post, error) {
|
|
||||||
post, _, appErr := a.api.postService.GetPostsByIds(postIDs)
|
|
||||||
return post, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) SendEphemeralPost(userID string, post *mm_model.Post) {
|
|
||||||
*post = *a.api.postService.SendEphemeralPost(a.ctx, userID, post)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetPost(postID string) (*mm_model.Post, error) {
|
|
||||||
post, appErr := a.api.postService.GetPost(postID)
|
|
||||||
return post, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) DeletePost(postID string) (*mm_model.Post, error) {
|
|
||||||
post, appErr := a.api.postService.DeletePost(a.ctx, postID, playbooksProductID)
|
|
||||||
return post, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) UpdatePost(post *mm_model.Post) (*mm_model.Post, error) {
|
|
||||||
post, appErr := a.api.postService.UpdatePost(a.ctx, post, false)
|
|
||||||
return post, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// User service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetUserByID(userID string) (*mm_model.User, error) {
|
|
||||||
user, appErr := a.api.userService.GetUser(userID)
|
|
||||||
return user, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetUserByUsername(name string) (*mm_model.User, error) {
|
|
||||||
user, appErr := a.api.userService.GetUserByUsername(name)
|
|
||||||
return user, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetUserByEmail(email string) (*mm_model.User, error) {
|
|
||||||
user, appErr := a.api.userService.GetUserByEmail(email)
|
|
||||||
return user, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) UpdateUser(user *mm_model.User) (*mm_model.User, error) {
|
|
||||||
user, appErr := a.api.userService.UpdateUser(a.ctx, user, true)
|
|
||||||
return user, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetUsersFromProfiles(options *mm_model.UserGetOptions) ([]*mm_model.User, error) {
|
|
||||||
user, appErr := a.api.userService.GetUsersFromProfiles(options)
|
|
||||||
return user, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Team service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error) {
|
|
||||||
member, appErr := a.api.teamService.GetMember(teamID, userID)
|
|
||||||
return member, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) CreateMember(teamID string, userID string) (*mm_model.TeamMember, error) {
|
|
||||||
member, appErr := a.api.teamService.CreateMember(a.ctx, teamID, userID)
|
|
||||||
return member, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetGroup(groupID string) (*model.Group, error) {
|
|
||||||
group, appErr := a.api.teamService.GetGroup(groupID)
|
|
||||||
return group, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetTeam(teamID string) (*mm_model.Team, error) {
|
|
||||||
team, appErr := a.api.teamService.GetTeam(teamID)
|
|
||||||
return team, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetGroupMemberUsers(groupID string, page, perPage int) ([]*mm_model.User, error) {
|
|
||||||
users, appErr := a.api.teamService.GetGroupMemberUsers(groupID, page, perPage)
|
|
||||||
return users, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Permissions service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) HasPermissionTo(userID string, permission *mm_model.Permission) bool {
|
|
||||||
return a.api.permissionsService.HasPermissionTo(userID, permission)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool {
|
|
||||||
return a.api.permissionsService.HasPermissionToTeam(userID, teamID, permission)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) HasPermissionToChannel(askingUserID string, channelID string, permission *mm_model.Permission) bool {
|
|
||||||
return a.api.permissionsService.HasPermissionToChannel(askingUserID, channelID, permission)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) RolesGrantPermission(roleNames []string, permissionID string) bool {
|
|
||||||
return a.api.permissionsService.RolesGrantPermission(roleNames, permissionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Bot service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) {
|
|
||||||
return a.api.botService.EnsureBot(a.ctx, playbooksProductID, bot)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// License service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetLicense() *mm_model.License {
|
|
||||||
return a.api.licenseService.GetLicense()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) error {
|
|
||||||
return normalizeAppErr(a.api.licenseService.RequestTrialLicense(requesterID, users, termsAccepted, receiveEmailsAccepted))
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// FileInfoStore service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error) {
|
|
||||||
fi, appErr := a.api.fileInfoStoreService.GetFileInfo(fileID)
|
|
||||||
return fi, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Cluster store.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) {
|
|
||||||
a.api.clusterService.PublishWebSocketEvent(playbooksProductID, event, payload, broadcast)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) PublishPluginClusterEvent(ev mm_model.PluginClusterEvent, opts mm_model.PluginClusterEventSendOptions) error {
|
|
||||||
return a.api.clusterService.PublishPluginClusterEvent(playbooksProductID, ev, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Cloud service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) {
|
|
||||||
return a.api.cloudService.GetCloudLimits()
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Config service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetConfig() *mm_model.Config {
|
|
||||||
cfg := a.api.configService.Config().Clone()
|
|
||||||
cfg.Sanitize()
|
|
||||||
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) LoadPluginConfiguration(dest any) error {
|
|
||||||
finalConfig := make(map[string]any)
|
|
||||||
|
|
||||||
// If we have settings given we override the defaults with them
|
|
||||||
for setting, value := range a.api.configService.Config().PluginSettings.Plugins[playbooksProductID] {
|
|
||||||
finalConfig[strings.ToLower(setting)] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginSettingsJSONBytes, err := json.Marshal(finalConfig)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Error("Error marshaling config for plugin")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(pluginSettingsJSONBytes, dest)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Error("Error unmarshaling config for plugin")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) SavePluginConfig(pluginConfig map[string]any) error {
|
|
||||||
cfg := a.GetConfig()
|
|
||||||
cfg.PluginSettings.Plugins["playbooks"] = pluginConfig
|
|
||||||
_, _, err := a.api.configService.SaveConfig(cfg, true)
|
|
||||||
|
|
||||||
return normalizeAppErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// KVStore service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) {
|
|
||||||
b, appErr := a.api.kvStoreService.SetPluginKeyWithOptions(playbooksProductID, key, value, options)
|
|
||||||
return b, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) KVGet(key string) ([]byte, error) {
|
|
||||||
data, appErr := a.api.kvStoreService.KVGet(playbooksProductID, key)
|
|
||||||
return data, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) KVDelete(key string) error {
|
|
||||||
appErr := a.api.kvStoreService.KVDelete(playbooksProductID, key)
|
|
||||||
return normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
func (a *serviceAPIAdapter) KVList(page, perPage int) ([]string, error) {
|
|
||||||
data, appErr := a.api.kvStoreService.KVList(playbooksProductID, page, perPage)
|
|
||||||
return data, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get gets the value for the given key into the given interface.
|
|
||||||
//
|
|
||||||
// An error is returned only if the value cannot be fetched. A non-existent key will return no
|
|
||||||
// error, with nothing written to the given interface.
|
|
||||||
//
|
|
||||||
// Minimum server version: 5.2
|
|
||||||
func (a *serviceAPIAdapter) Get(key string, o interface{}) error {
|
|
||||||
data, appErr := a.api.kvStoreService.KVGet(playbooksProductID, key)
|
|
||||||
if appErr != nil {
|
|
||||||
return normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytesOut, ok := o.(*[]byte); ok {
|
|
||||||
*bytesOut = data
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(data, o); err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to unmarshal value for key %s", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Store service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetMasterDB() (*sql.DB, error) {
|
|
||||||
return a.api.storeService.GetMasterDB(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DriverName returns the driver name for the datasource.
|
|
||||||
func (a *serviceAPIAdapter) DriverName() string {
|
|
||||||
return *a.api.configService.Config().SqlSettings.DriverName
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// System service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetDiagnosticID() string {
|
|
||||||
return a.api.systemService.GetDiagnosticId()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetServerVersion() string {
|
|
||||||
return model.CurrentVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Router service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) RegisterRouter(sub *mux.Router) {
|
|
||||||
a.api.routerService.RegisterRouter(playbooksProductName, sub)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Preferences service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) {
|
|
||||||
p, appErr := a.api.preferencesService.GetPreferencesForUser(userID)
|
|
||||||
return p, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error {
|
|
||||||
appErr := a.api.preferencesService.UpdatePreferencesForUser(userID, preferences)
|
|
||||||
return normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error {
|
|
||||||
appErr := a.api.preferencesService.DeletePreferencesForUser(userID, preferences)
|
|
||||||
return normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Session service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetSession(sessionID string) (*mm_model.Session, error) {
|
|
||||||
session, appErr := a.api.sessionService.GetSessionById(sessionID)
|
|
||||||
return session, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Frontend service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) OpenInteractiveDialog(dialog model.OpenDialogRequest) error {
|
|
||||||
return normalizeAppErr(a.api.frontendService.OpenInteractiveDialog(dialog))
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Command service.
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) Execute(command *mm_model.CommandArgs) (*mm_model.CommandResponse, error) {
|
|
||||||
user, err := a.GetUserByID(command.UserId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
command.T = i18n.GetUserTranslations(user.Locale)
|
|
||||||
command.SiteURL = *a.GetConfig().ServiceSettings.SiteURL
|
|
||||||
response, appErr := a.api.commandService.ExecuteCommand(a.ctx, command)
|
|
||||||
return response, normalizeAppErr(appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) RegisterCommand(command *mm_model.Command) error {
|
|
||||||
return a.api.commandService.RegisterProductCommand(playbooksProductName, command)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) IsEnterpriseReady() bool {
|
|
||||||
result, _ := strconv.ParseBool(model.BuildEnterpriseReady)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Threads service
|
|
||||||
//
|
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) RegisterCollectionAndTopic(collectionType, topicType string) error {
|
|
||||||
return a.api.threadsService.RegisterCollectionAndTopic(playbooksProductID, collectionType, topicType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the adapter implements ServicesAPI.
|
|
||||||
var _ playbooks.ServicesAPI = &serviceAPIAdapter{}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package imports
|
|
||||||
|
|
||||||
import (
|
|
||||||
// Needed to ensure the init() method in the Playbooks product is run.
|
|
||||||
// This file is copied to the mmserver imports package via makefile.
|
|
||||||
_ "github.com/mattermost/mattermost/server/v8/playbooks/product"
|
|
||||||
)
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package product
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/mattermost/logr/v2"
|
|
||||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LogrusHook is a logrus.Hook for emitting plugin logs through the RPC API for inclusion in the
|
|
||||||
// server logs.
|
|
||||||
//
|
|
||||||
// To configure the default Logrus logger for use with plugin logging, simply invoke:
|
|
||||||
//
|
|
||||||
// pluginapi.ConfigureLogrus(logrus.StandardLogger(), pluginAPIClient)
|
|
||||||
//
|
|
||||||
// Alternatively, construct your own logger to pass to pluginapi.ConfigureLogrus.
|
|
||||||
type LogrusHook struct {
|
|
||||||
log mlog.LoggerIFace
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLogrusHook creates a new instance of LogrusHook.
|
|
||||||
func NewLogrusHook(log mlog.LoggerIFace) *LogrusHook {
|
|
||||||
return &LogrusHook{
|
|
||||||
log: log,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Levels allows LogrusHook to process any log level.
|
|
||||||
func (lh *LogrusHook) Levels() []logrus.Level {
|
|
||||||
return logrus.AllLevels
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire proxies logrus entries through the plugin API at the appropriate level.
|
|
||||||
func (lh *LogrusHook) Fire(entry *logrus.Entry) error {
|
|
||||||
fields := []logr.Field{}
|
|
||||||
for key, value := range entry.Data {
|
|
||||||
field := logr.Field{
|
|
||||||
Key: key,
|
|
||||||
Interface: value,
|
|
||||||
}
|
|
||||||
if key == "error" {
|
|
||||||
field.Type = logr.ErrorType
|
|
||||||
}
|
|
||||||
|
|
||||||
fields = append(fields, field)
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry.Caller != nil {
|
|
||||||
fields = append(fields,
|
|
||||||
logr.Field{
|
|
||||||
Key: "plugin_caller",
|
|
||||||
String: fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
switch entry.Level {
|
|
||||||
case logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel:
|
|
||||||
lh.log.Error(entry.Message, fields...)
|
|
||||||
case logrus.WarnLevel:
|
|
||||||
lh.log.Warn(entry.Message, fields...)
|
|
||||||
case logrus.InfoLevel:
|
|
||||||
lh.log.Info(entry.Message, fields...)
|
|
||||||
case logrus.DebugLevel, logrus.TraceLevel:
|
|
||||||
lh.log.Debug(entry.Message, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigureLogrus configures the given logrus logger with a hook to proxy through the RPC API,
|
|
||||||
// discarding the default output to avoid duplicating the events across the standard STDOUT proxy.
|
|
||||||
func ConfigureLogrus(logger *logrus.Logger, log mlog.LoggerIFace) {
|
|
||||||
hook := NewLogrusHook(log)
|
|
||||||
logger.Hooks.Add(hook)
|
|
||||||
logger.SetOutput(io.Discard)
|
|
||||||
logrus.SetReportCaller(true)
|
|
||||||
|
|
||||||
// By default, log everything to the server, and let it decide what gets through.
|
|
||||||
logrus.SetLevel(logrus.TraceLevel)
|
|
||||||
}
|
|
||||||
@@ -1,818 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package product
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/public/plugin"
|
|
||||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
||||||
mmapp "github.com/mattermost/mattermost/server/v8/channels/app"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/channels/product"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/product/pluginapi/cluster"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/api"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/bot"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/command"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/config"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/enterprise"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/metrics"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/scheduler"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/sqlstore"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/telemetry"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
playbooksProductName = "playbooks"
|
|
||||||
playbooksProductID = "playbooks"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
updateMetricsTaskFrequency = 15 * time.Minute
|
|
||||||
|
|
||||||
metricsExposePort = ":9093"
|
|
||||||
|
|
||||||
// Topic represents a start of a thread. In playbooks we support 2 types of topics:
|
|
||||||
// status topic - indicating the start of the thread below status update and
|
|
||||||
// task topic - indicating the start of the thread below task(checklist item)
|
|
||||||
TopicTypeStatus = "status"
|
|
||||||
TopicTypeTask = "task"
|
|
||||||
|
|
||||||
// Collection is a group of topics and their corresponding threads.
|
|
||||||
// In Playbooks we support a single type of collection - a run
|
|
||||||
CollectionTypeRun = "run"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ServerKey product.ServiceKey = "server"
|
|
||||||
|
|
||||||
const (
|
|
||||||
rudderDataplaneURL = "https://pdat.matterlytics.com"
|
|
||||||
rudderKeyProd = "1ag0Mv7LPf5uJNhcnKomqg0ENFd"
|
|
||||||
rudderKeyTest = "1Zu3mOF6U6M9zeaJsfmmhYigWLt"
|
|
||||||
|
|
||||||
// These are placeholders to allow the existing release pipelines to run without failing to
|
|
||||||
// insert the values that are now hard-coded above. Remove this once we converge on the
|
|
||||||
// unified delivery pipeline in GitHub.
|
|
||||||
_ = "placeholder_rudder_dataplane_url"
|
|
||||||
_ = "placeholder_playbooks_rudder_key"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errServiceTypeAssert = errors.New("type assertion failed")
|
|
||||||
|
|
||||||
type TelemetryClient interface {
|
|
||||||
app.PlaybookRunTelemetry
|
|
||||||
app.PlaybookTelemetry
|
|
||||||
app.GenericTelemetry
|
|
||||||
bot.Telemetry
|
|
||||||
app.UserInfoTelemetry
|
|
||||||
app.ChannelActionTelemetry
|
|
||||||
app.CategoryTelemetry
|
|
||||||
Enable() error
|
|
||||||
Disable() error
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
product.RegisterProduct(playbooksProductName, product.Manifest{
|
|
||||||
Initializer: newPlaybooksProduct,
|
|
||||||
Dependencies: map[product.ServiceKey]struct{}{
|
|
||||||
product.TeamKey: {},
|
|
||||||
product.ChannelKey: {},
|
|
||||||
product.UserKey: {},
|
|
||||||
product.PostKey: {},
|
|
||||||
product.BotKey: {},
|
|
||||||
product.ClusterKey: {},
|
|
||||||
product.ConfigKey: {},
|
|
||||||
product.LogKey: {},
|
|
||||||
product.LicenseKey: {},
|
|
||||||
product.FilestoreKey: {},
|
|
||||||
product.FileInfoStoreKey: {},
|
|
||||||
product.RouterKey: {},
|
|
||||||
product.CloudKey: {},
|
|
||||||
product.KVStoreKey: {},
|
|
||||||
product.StoreKey: {},
|
|
||||||
product.SystemKey: {},
|
|
||||||
product.PreferencesKey: {},
|
|
||||||
product.SessionKey: {},
|
|
||||||
product.FrontendKey: {},
|
|
||||||
product.CommandKey: {},
|
|
||||||
product.ThreadsKey: {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type playbooksProduct struct {
|
|
||||||
server *mmapp.Server
|
|
||||||
teamService product.TeamService
|
|
||||||
channelService product.ChannelService
|
|
||||||
userService product.UserService
|
|
||||||
postService product.PostService
|
|
||||||
permissionsService product.PermissionService
|
|
||||||
botService product.BotService
|
|
||||||
clusterService product.ClusterService
|
|
||||||
configService product.ConfigService
|
|
||||||
logger mlog.LoggerIFace
|
|
||||||
licenseService product.LicenseService
|
|
||||||
filestoreService product.FilestoreService
|
|
||||||
fileInfoStoreService product.FileInfoStoreService
|
|
||||||
routerService product.RouterService
|
|
||||||
cloudService product.CloudService
|
|
||||||
kvStoreService product.KVStoreService
|
|
||||||
storeService product.StoreService
|
|
||||||
systemService product.SystemService
|
|
||||||
preferencesService product.PreferencesService
|
|
||||||
hooksService product.HooksService
|
|
||||||
sessionService product.SessionService
|
|
||||||
frontendService product.FrontendService
|
|
||||||
commandService product.CommandService
|
|
||||||
threadsService product.ThreadsService
|
|
||||||
|
|
||||||
handler *api.Handler
|
|
||||||
config *config.ServiceImpl
|
|
||||||
playbookRunService app.PlaybookRunService
|
|
||||||
playbookService app.PlaybookService
|
|
||||||
permissions *app.PermissionsService
|
|
||||||
channelActionService app.ChannelActionService
|
|
||||||
categoryService app.CategoryService
|
|
||||||
bot *bot.Bot
|
|
||||||
userInfoStore app.UserInfoStore
|
|
||||||
telemetryClient TelemetryClient
|
|
||||||
licenseChecker app.LicenseChecker
|
|
||||||
metricsService *metrics.Metrics
|
|
||||||
playbookStore app.PlaybookStore
|
|
||||||
playbookRunStore app.PlaybookRunStore
|
|
||||||
metricsServer *metrics.Service
|
|
||||||
metricsUpdaterTask *scheduler.ScheduledTask
|
|
||||||
|
|
||||||
serviceAdapter playbooks.ServicesAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPlaybooksProduct(services map[product.ServiceKey]interface{}) (product.Product, error) {
|
|
||||||
playbooks := &playbooksProduct{}
|
|
||||||
err := playbooks.setProductServices(services)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
playbooks.server = services[ServerKey].(*mmapp.Server)
|
|
||||||
|
|
||||||
playbooks.serviceAdapter = newServiceAPIAdapter(playbooks)
|
|
||||||
|
|
||||||
return playbooks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *playbooksProduct) setProductServices(services map[product.ServiceKey]interface{}) error {
|
|
||||||
for key, service := range services {
|
|
||||||
switch key {
|
|
||||||
case product.TeamKey:
|
|
||||||
teamService, ok := service.(product.TeamService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.teamService = teamService
|
|
||||||
case product.ChannelKey:
|
|
||||||
channelService, ok := service.(product.ChannelService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.channelService = channelService
|
|
||||||
case product.UserKey:
|
|
||||||
userService, ok := service.(product.UserService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.userService = userService
|
|
||||||
case product.PostKey:
|
|
||||||
postService, ok := service.(product.PostService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.postService = postService
|
|
||||||
case product.PermissionsKey:
|
|
||||||
permissionsService, ok := service.(product.PermissionService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.permissionsService = permissionsService
|
|
||||||
case product.BotKey:
|
|
||||||
botService, ok := service.(product.BotService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.botService = botService
|
|
||||||
case product.ClusterKey:
|
|
||||||
clusterService, ok := service.(product.ClusterService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.clusterService = clusterService
|
|
||||||
case product.ConfigKey:
|
|
||||||
configService, ok := service.(product.ConfigService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.configService = configService
|
|
||||||
case product.LogKey:
|
|
||||||
logger, ok := service.(mlog.LoggerIFace)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.logger = logger.With(mlog.String("product", playbooksProductName))
|
|
||||||
case product.LicenseKey:
|
|
||||||
licenseService, ok := service.(product.LicenseService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.licenseService = licenseService
|
|
||||||
case product.FilestoreKey:
|
|
||||||
filestoreService, ok := service.(product.FilestoreService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.filestoreService = filestoreService
|
|
||||||
case product.FileInfoStoreKey:
|
|
||||||
fileInfoStoreService, ok := service.(product.FileInfoStoreService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.fileInfoStoreService = fileInfoStoreService
|
|
||||||
case product.RouterKey:
|
|
||||||
routerService, ok := service.(product.RouterService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.routerService = routerService
|
|
||||||
case product.CloudKey:
|
|
||||||
cloudService, ok := service.(product.CloudService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.cloudService = cloudService
|
|
||||||
case product.KVStoreKey:
|
|
||||||
kvStoreService, ok := service.(product.KVStoreService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.kvStoreService = kvStoreService
|
|
||||||
case product.StoreKey:
|
|
||||||
storeService, ok := service.(product.StoreService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.storeService = storeService
|
|
||||||
case product.SystemKey:
|
|
||||||
systemService, ok := service.(product.SystemService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.systemService = systemService
|
|
||||||
case product.PreferencesKey:
|
|
||||||
preferencesService, ok := service.(product.PreferencesService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.preferencesService = preferencesService
|
|
||||||
case product.HooksKey:
|
|
||||||
hooksService, ok := service.(product.HooksService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.hooksService = hooksService
|
|
||||||
case product.SessionKey:
|
|
||||||
sessionService, ok := service.(product.SessionService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.sessionService = sessionService
|
|
||||||
case product.FrontendKey:
|
|
||||||
frontendService, ok := service.(product.FrontendService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.frontendService = frontendService
|
|
||||||
case product.CommandKey:
|
|
||||||
commandService, ok := service.(product.CommandService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.commandService = commandService
|
|
||||||
case product.ThreadsKey:
|
|
||||||
threadsService, ok := service.(product.ThreadsService)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
|
||||||
}
|
|
||||||
pp.threadsService = threadsService
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *playbooksProduct) Start() error {
|
|
||||||
logger := logrus.StandardLogger()
|
|
||||||
ConfigureLogrus(logger, pp.logger)
|
|
||||||
|
|
||||||
botID, err := pp.serviceAdapter.EnsureBot(&model.Bot{
|
|
||||||
Username: "playbooks",
|
|
||||||
DisplayName: "Playbooks",
|
|
||||||
Description: "Playbooks bot.",
|
|
||||||
OwnerId: "playbooks",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to ensure bot")
|
|
||||||
}
|
|
||||||
|
|
||||||
pp.config = config.NewConfigService(pp.serviceAdapter)
|
|
||||||
err = pp.config.UpdateConfiguration(func(c *config.Configuration) {
|
|
||||||
c.BotUserID = botID
|
|
||||||
c.AdminLogLevel = "debug"
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed save bot to config")
|
|
||||||
}
|
|
||||||
|
|
||||||
pp.handler = api.NewHandler(pp.config)
|
|
||||||
|
|
||||||
rudderWriteKey := ""
|
|
||||||
switch model.GetServiceEnvironment() {
|
|
||||||
case model.ServiceEnvironmentProduction:
|
|
||||||
rudderWriteKey = rudderKeyProd
|
|
||||||
case model.ServiceEnvironmentTest:
|
|
||||||
rudderWriteKey = rudderKeyTest
|
|
||||||
case model.ServiceEnvironmentDev:
|
|
||||||
}
|
|
||||||
|
|
||||||
if rudderWriteKey == "" {
|
|
||||||
logrus.Warn("Rudder credentials are not set. Disabling analytics.")
|
|
||||||
pp.telemetryClient = &telemetry.NoopTelemetry{}
|
|
||||||
} else {
|
|
||||||
logrus.Info("Rudder credentials are set. Enabling analytics.")
|
|
||||||
diagnosticID := pp.serviceAdapter.GetDiagnosticID()
|
|
||||||
serverVersion := pp.serviceAdapter.GetServerVersion()
|
|
||||||
pp.telemetryClient, err = telemetry.NewRudder(rudderDataplaneURL, rudderWriteKey, diagnosticID, serverVersion)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed init telemetry client")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleTelemetry := func() {
|
|
||||||
diagnosticsFlag := pp.serviceAdapter.GetConfig().LogSettings.EnableDiagnostics
|
|
||||||
telemetryEnabled := diagnosticsFlag != nil && *diagnosticsFlag
|
|
||||||
|
|
||||||
if telemetryEnabled {
|
|
||||||
if err = pp.telemetryClient.Enable(); err != nil {
|
|
||||||
logrus.WithError(err).Error("Telemetry could not be enabled")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = pp.telemetryClient.Disable(); err != nil {
|
|
||||||
logrus.WithError(err).Error("Telemetry could not be disabled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleTelemetry()
|
|
||||||
pp.config.RegisterConfigChangeListener(toggleTelemetry)
|
|
||||||
|
|
||||||
apiClient := sqlstore.NewClient(pp.serviceAdapter)
|
|
||||||
pp.bot = bot.New(pp.serviceAdapter, pp.config.GetConfiguration().BotUserID, pp.config, pp.telemetryClient)
|
|
||||||
scheduler := cluster.GetJobOnceScheduler(pp.serviceAdapter)
|
|
||||||
|
|
||||||
sqlStore, err := sqlstore.New(apiClient, scheduler)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed creating the SQL store")
|
|
||||||
}
|
|
||||||
|
|
||||||
pp.playbookRunStore = sqlstore.NewPlaybookRunStore(apiClient, sqlStore)
|
|
||||||
pp.playbookStore = sqlstore.NewPlaybookStore(apiClient, sqlStore)
|
|
||||||
statsStore := sqlstore.NewStatsStore(apiClient, sqlStore)
|
|
||||||
pp.userInfoStore = sqlstore.NewUserInfoStore(sqlStore)
|
|
||||||
channelActionStore := sqlstore.NewChannelActionStore(apiClient, sqlStore)
|
|
||||||
categoryStore := sqlstore.NewCategoryStore(apiClient, sqlStore)
|
|
||||||
|
|
||||||
pp.handler = api.NewHandler(pp.config)
|
|
||||||
|
|
||||||
pp.playbookService = app.NewPlaybookService(pp.playbookStore, pp.bot, pp.telemetryClient, pp.serviceAdapter, pp.metricsService)
|
|
||||||
|
|
||||||
keywordsThreadIgnorer := app.NewKeywordsThreadIgnorer()
|
|
||||||
pp.channelActionService = app.NewChannelActionsService(pp.serviceAdapter, pp.bot, pp.config, channelActionStore, pp.playbookService, keywordsThreadIgnorer, pp.telemetryClient)
|
|
||||||
pp.categoryService = app.NewCategoryService(categoryStore, pp.serviceAdapter, pp.telemetryClient)
|
|
||||||
|
|
||||||
pp.licenseChecker = enterprise.NewLicenseChecker(pp.serviceAdapter)
|
|
||||||
|
|
||||||
pp.playbookRunService = app.NewPlaybookRunService(
|
|
||||||
pp.playbookRunStore,
|
|
||||||
pp.bot,
|
|
||||||
pp.config,
|
|
||||||
scheduler,
|
|
||||||
pp.telemetryClient,
|
|
||||||
pp.telemetryClient,
|
|
||||||
pp.serviceAdapter,
|
|
||||||
pp.playbookService,
|
|
||||||
pp.channelActionService,
|
|
||||||
pp.licenseChecker,
|
|
||||||
pp.metricsService,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = scheduler.SetCallback(pp.playbookRunService.HandleReminder); err != nil {
|
|
||||||
logrus.WithError(err).Error("JobOnceScheduler could not add the playbookRunService's HandleReminder")
|
|
||||||
}
|
|
||||||
if err = scheduler.Start(); err != nil {
|
|
||||||
logrus.WithError(err).Error("JobOnceScheduler could not start")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrations use the scheduler, so they have to be run after playbookRunService and scheduler have started
|
|
||||||
mutex, err := cluster.NewMutex(pp.serviceAdapter, "IR_dbMutex")
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed creating cluster mutex")
|
|
||||||
}
|
|
||||||
mutex.Lock()
|
|
||||||
if err = sqlStore.RunMigrations(); err != nil {
|
|
||||||
mutex.Unlock()
|
|
||||||
return errors.Wrapf(err, "failed to run migrations")
|
|
||||||
}
|
|
||||||
mutex.Unlock()
|
|
||||||
|
|
||||||
pp.permissions = app.NewPermissionsService(
|
|
||||||
pp.playbookService,
|
|
||||||
pp.playbookRunService,
|
|
||||||
pp.serviceAdapter,
|
|
||||||
pp.config,
|
|
||||||
pp.licenseChecker,
|
|
||||||
)
|
|
||||||
|
|
||||||
// register collections and topics.
|
|
||||||
// TODO bump the minimum server version
|
|
||||||
if err = pp.serviceAdapter.RegisterCollectionAndTopic(CollectionTypeRun, TopicTypeStatus); err != nil {
|
|
||||||
logrus.WithError(err).WithField("collection_type", CollectionTypeRun).WithField("topic_type", TopicTypeStatus).Warnf("failed to register collection and topic")
|
|
||||||
}
|
|
||||||
if err = pp.serviceAdapter.RegisterCollectionAndTopic(CollectionTypeRun, TopicTypeTask); err != nil {
|
|
||||||
logrus.WithError(err).WithField("collection_type", CollectionTypeRun).WithField("topic_type", TopicTypeTask).Warnf("failed to register collection and topic")
|
|
||||||
}
|
|
||||||
|
|
||||||
api.NewGraphQLHandler(
|
|
||||||
pp.handler.APIRouter,
|
|
||||||
pp.playbookService,
|
|
||||||
pp.playbookRunService,
|
|
||||||
pp.categoryService,
|
|
||||||
pp.serviceAdapter,
|
|
||||||
pp.config,
|
|
||||||
pp.permissions,
|
|
||||||
pp.playbookStore,
|
|
||||||
pp.licenseChecker,
|
|
||||||
)
|
|
||||||
api.NewPlaybookHandler(
|
|
||||||
pp.handler.APIRouter,
|
|
||||||
pp.playbookService,
|
|
||||||
pp.serviceAdapter,
|
|
||||||
pp.config,
|
|
||||||
pp.permissions,
|
|
||||||
)
|
|
||||||
api.NewPlaybookRunHandler(
|
|
||||||
pp.handler.APIRouter,
|
|
||||||
pp.playbookRunService,
|
|
||||||
pp.playbookService,
|
|
||||||
pp.permissions,
|
|
||||||
pp.licenseChecker,
|
|
||||||
pp.serviceAdapter,
|
|
||||||
pp.bot,
|
|
||||||
pp.config,
|
|
||||||
)
|
|
||||||
api.NewStatsHandler(
|
|
||||||
pp.handler.APIRouter,
|
|
||||||
pp.serviceAdapter,
|
|
||||||
statsStore,
|
|
||||||
pp.playbookService,
|
|
||||||
pp.permissions,
|
|
||||||
pp.licenseChecker,
|
|
||||||
)
|
|
||||||
api.NewBotHandler(
|
|
||||||
pp.handler.APIRouter,
|
|
||||||
pp.serviceAdapter, pp.bot,
|
|
||||||
pp.config,
|
|
||||||
pp.playbookRunService,
|
|
||||||
pp.userInfoStore,
|
|
||||||
)
|
|
||||||
api.NewTelemetryHandler(
|
|
||||||
pp.handler.APIRouter,
|
|
||||||
pp.playbookRunService,
|
|
||||||
pp.serviceAdapter,
|
|
||||||
pp.telemetryClient,
|
|
||||||
pp.playbookService,
|
|
||||||
pp.telemetryClient,
|
|
||||||
pp.telemetryClient,
|
|
||||||
pp.telemetryClient,
|
|
||||||
pp.permissions,
|
|
||||||
)
|
|
||||||
api.NewSignalHandler(
|
|
||||||
pp.handler.APIRouter,
|
|
||||||
pp.serviceAdapter,
|
|
||||||
pp.playbookRunService,
|
|
||||||
pp.playbookService,
|
|
||||||
keywordsThreadIgnorer,
|
|
||||||
)
|
|
||||||
api.NewSettingsHandler(
|
|
||||||
pp.handler.APIRouter,
|
|
||||||
pp.serviceAdapter,
|
|
||||||
pp.config,
|
|
||||||
)
|
|
||||||
api.NewActionsHandler(
|
|
||||||
pp.handler.APIRouter,
|
|
||||||
pp.channelActionService,
|
|
||||||
pp.serviceAdapter,
|
|
||||||
pp.permissions,
|
|
||||||
)
|
|
||||||
api.NewCategoryHandler(
|
|
||||||
pp.handler.APIRouter,
|
|
||||||
pp.serviceAdapter,
|
|
||||||
pp.categoryService,
|
|
||||||
pp.playbookService,
|
|
||||||
pp.playbookRunService,
|
|
||||||
)
|
|
||||||
|
|
||||||
isTestingEnabled := false
|
|
||||||
flag := pp.serviceAdapter.GetConfig().ServiceSettings.EnableTesting
|
|
||||||
if flag != nil {
|
|
||||||
isTestingEnabled = *flag
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = command.RegisterCommands(pp.serviceAdapter.RegisterCommand, isTestingEnabled); err != nil {
|
|
||||||
return errors.Wrapf(err, "failed register commands")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pp.hooksService.RegisterHooks(playbooksProductName, pp); err != nil {
|
|
||||||
return fmt.Errorf("failed to register hooks: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
enableMetrics := pp.configService.Config().MetricsSettings.Enable
|
|
||||||
if enableMetrics != nil && *enableMetrics {
|
|
||||||
pp.metricsService = newMetricsInstance()
|
|
||||||
// run metrics server to expose data
|
|
||||||
pp.runMetricsServer()
|
|
||||||
// run metrics updater recurring task
|
|
||||||
pp.runMetricsUpdaterTask(pp.playbookStore, pp.playbookRunStore, updateMetricsTaskFrequency)
|
|
||||||
// set error counter middleware handler
|
|
||||||
pp.handler.APIRouter.Use(pp.getErrorCounterHandler())
|
|
||||||
}
|
|
||||||
|
|
||||||
pp.routerService.RegisterRouter(playbooksProductName, pp.handler.APIRouter)
|
|
||||||
|
|
||||||
logrus.Debug("Playbooks product successfully started.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *playbooksProduct) Stop() error {
|
|
||||||
if pp.metricsServer != nil {
|
|
||||||
err := pp.metricsServer.Shutdown()
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Warn("unable to shut down metric server")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pp.metricsUpdaterTask != nil {
|
|
||||||
pp.metricsUpdaterTask.Cancel()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMetricsInstance() *metrics.Metrics {
|
|
||||||
// Init metrics
|
|
||||||
instanceInfo := metrics.InstanceInfo{
|
|
||||||
Version: model.BuildHash,
|
|
||||||
InstallationID: os.Getenv("MM_CLOUD_INSTALLATION_ID"),
|
|
||||||
}
|
|
||||||
return metrics.NewMetrics(instanceInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *playbooksProduct) runMetricsServer() {
|
|
||||||
logrus.WithField("port", metricsExposePort).Info("Starting Playbooks metrics server")
|
|
||||||
|
|
||||||
pp.metricsServer = metrics.NewMetricsServer(metricsExposePort, pp.metricsService)
|
|
||||||
// Run server to expose metrics
|
|
||||||
go func() {
|
|
||||||
err := pp.metricsServer.Run()
|
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
logrus.WithError(err).Error("Metrics server could not be started")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *playbooksProduct) runMetricsUpdaterTask(playbookStore app.PlaybookStore, playbookRunStore app.PlaybookRunStore, updateMetricsTaskFrequency time.Duration) {
|
|
||||||
metricsUpdater := func() {
|
|
||||||
if playbooksActiveTotal, err := playbookStore.GetPlaybooksActiveTotal(); err == nil {
|
|
||||||
pp.metricsService.ObservePlaybooksActiveTotal(playbooksActiveTotal)
|
|
||||||
} else {
|
|
||||||
logrus.WithError(err).Error("error updating metrics, playbooks_active_total")
|
|
||||||
}
|
|
||||||
|
|
||||||
if runsActiveTotal, err := playbookRunStore.GetRunsActiveTotal(); err == nil {
|
|
||||||
pp.metricsService.ObserveRunsActiveTotal(runsActiveTotal)
|
|
||||||
} else {
|
|
||||||
logrus.WithError(err).Error("error updating metrics, runs_active_total")
|
|
||||||
}
|
|
||||||
|
|
||||||
if remindersOverdueTotal, err := playbookRunStore.GetOverdueUpdateRunsTotal(); err == nil {
|
|
||||||
pp.metricsService.ObserveRemindersOutstandingTotal(remindersOverdueTotal)
|
|
||||||
} else {
|
|
||||||
logrus.WithError(err).Error("error updating metrics, reminders_outstanding_total")
|
|
||||||
}
|
|
||||||
|
|
||||||
if retrosOverdueTotal, err := playbookRunStore.GetOverdueRetroRunsTotal(); err == nil {
|
|
||||||
pp.metricsService.ObserveRetrosOutstandingTotal(retrosOverdueTotal)
|
|
||||||
} else {
|
|
||||||
logrus.WithError(err).Error("error updating metrics, retros_outstanding_total")
|
|
||||||
}
|
|
||||||
|
|
||||||
if followersActiveTotal, err := playbookRunStore.GetFollowersActiveTotal(); err == nil {
|
|
||||||
pp.metricsService.ObserveFollowersActiveTotal(followersActiveTotal)
|
|
||||||
} else {
|
|
||||||
logrus.WithError(err).Error("error updating metrics, followers_active_total")
|
|
||||||
}
|
|
||||||
|
|
||||||
if participantsActiveTotal, err := playbookRunStore.GetParticipantsActiveTotal(); err == nil {
|
|
||||||
pp.metricsService.ObserveParticipantsActiveTotal(participantsActiveTotal)
|
|
||||||
} else {
|
|
||||||
logrus.WithError(err).Error("error updating metrics, participants_active_total")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pp.metricsUpdaterTask = scheduler.CreateRecurringTask("metricsUpdater", metricsUpdater, updateMetricsTaskFrequency)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *playbooksProduct) getErrorCounterHandler() func(next http.Handler) http.Handler {
|
|
||||||
return func(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
recorder := &StatusRecorder{
|
|
||||||
ResponseWriter: w,
|
|
||||||
Status: 200,
|
|
||||||
}
|
|
||||||
next.ServeHTTP(recorder, r)
|
|
||||||
if recorder.Status < 200 || recorder.Status > 299 {
|
|
||||||
pp.metricsService.IncrementErrorsCount(1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatusRecorder struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
Status int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StatusRecorder) WriteHeader(status int) {
|
|
||||||
r.Status = status
|
|
||||||
r.ResponseWriter.WriteHeader(status)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP routes incoming HTTP requests to the plugin's REST API.
|
|
||||||
func (pp *playbooksProduct) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
pp.handler.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// These callbacks are called by the suite automatically
|
|
||||||
//
|
|
||||||
|
|
||||||
func (pp *playbooksProduct) OnConfigurationChange() error {
|
|
||||||
if pp.config == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return pp.config.OnConfigurationChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecuteCommand executes a command that has been previously registered via the RegisterCommand.
|
|
||||||
func (pp *playbooksProduct) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
|
||||||
runner := command.NewCommandRunner(c, args, pp.serviceAdapter, pp.bot,
|
|
||||||
pp.playbookRunService, pp.playbookService, pp.config, pp.userInfoStore, pp.telemetryClient, pp.permissions)
|
|
||||||
|
|
||||||
if err := runner.Execute(); err != nil {
|
|
||||||
return nil, model.NewAppError("Playbooks.ExecuteCommand", "app.command.execute.error", nil, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.CommandResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *playbooksProduct) UserHasJoinedChannel(c *plugin.Context, channelMember *model.ChannelMember, actor *model.User) {
|
|
||||||
actorID := ""
|
|
||||||
if actor != nil && actor.Id != channelMember.UserId {
|
|
||||||
actorID = actor.Id
|
|
||||||
}
|
|
||||||
pp.channelActionService.UserHasJoinedChannel(channelMember.UserId, channelMember.ChannelId, actorID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *playbooksProduct) MessageHasBeenPosted(c *plugin.Context, post *model.Post) {
|
|
||||||
pp.channelActionService.MessageHasBeenPosted(post)
|
|
||||||
pp.playbookRunService.MessageHasBeenPosted(post)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *playbooksProduct) UserHasPermissionToCollection(c *plugin.Context, userID string, collectionType, collectionID string, permission *model.Permission) (bool, error) {
|
|
||||||
if collectionType != CollectionTypeRun {
|
|
||||||
return false, errors.Errorf("collection %s is not registered by playbooks", collectionType)
|
|
||||||
}
|
|
||||||
|
|
||||||
run, err := pp.playbookRunService.GetPlaybookRun(collectionID)
|
|
||||||
if err != nil {
|
|
||||||
return false, errors.Wrapf(err, "No run with id - %s", collectionID)
|
|
||||||
}
|
|
||||||
return pp.permissions.HasPermissionsToRun(userID, run, permission), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *playbooksProduct) GetAllCollectionIDsForUser(c *plugin.Context, userID, collectionType string) ([]string, error) {
|
|
||||||
if collectionType != CollectionTypeRun {
|
|
||||||
return nil, errors.Errorf("collection %s is not registered by playbooks", collectionType)
|
|
||||||
}
|
|
||||||
|
|
||||||
ids, err := pp.playbookRunService.GetPlaybookRunIDsForUser(userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *playbooksProduct) GetAllUserIdsForCollection(c *plugin.Context, collectionType, collectionID string) ([]string, error) {
|
|
||||||
if collectionType != CollectionTypeRun {
|
|
||||||
return nil, errors.Errorf("collection %s is not registered by playbooks", collectionType)
|
|
||||||
}
|
|
||||||
|
|
||||||
run, err := pp.playbookRunService.GetPlaybookRun(collectionID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "No run with id - %s", collectionID)
|
|
||||||
}
|
|
||||||
followers, err := pp.playbookRunService.GetFollowers(collectionID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "can't get followers for run - %s", collectionID)
|
|
||||||
}
|
|
||||||
return mergeSlice(run.ParticipantIDs, followers), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *playbooksProduct) GetCollectionMetadataByIds(c *plugin.Context, collectionType string, collectionIDs []string) (map[string]*model.CollectionMetadata, error) {
|
|
||||||
if collectionType != CollectionTypeRun {
|
|
||||||
return nil, errors.Errorf("collection %s is not registered by playbooks", collectionType)
|
|
||||||
}
|
|
||||||
runsMetadata := map[string]*model.CollectionMetadata{}
|
|
||||||
runs, err := pp.playbookRunService.GetRunMetadataByIDs(collectionIDs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "can't get playbook run metadata by ids")
|
|
||||||
}
|
|
||||||
for _, run := range runs {
|
|
||||||
runsMetadata[run.ID] = &model.CollectionMetadata{
|
|
||||||
Id: run.ID,
|
|
||||||
CollectionType: CollectionTypeRun,
|
|
||||||
TeamId: run.TeamID,
|
|
||||||
Name: run.Name,
|
|
||||||
RelativeURL: app.GetRunDetailsRelativeURL(run.ID),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return runsMetadata, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *playbooksProduct) GetTopicMetadataByIds(c *plugin.Context, topicType string, topicIDs []string) (map[string]*model.TopicMetadata, error) {
|
|
||||||
topicsMetadata := map[string]*model.TopicMetadata{}
|
|
||||||
|
|
||||||
var getTopicMetadataByIDs func(topicIDs []string) ([]app.TopicMetadata, error)
|
|
||||||
switch topicType {
|
|
||||||
case TopicTypeStatus:
|
|
||||||
getTopicMetadataByIDs = pp.playbookRunService.GetStatusMetadataByIDs
|
|
||||||
case TopicTypeTask:
|
|
||||||
getTopicMetadataByIDs = pp.playbookRunService.GetTaskMetadataByIDs
|
|
||||||
default:
|
|
||||||
return map[string]*model.TopicMetadata{}, errors.Errorf("topic type %s is not registered by playbooks", topicType)
|
|
||||||
}
|
|
||||||
|
|
||||||
topics, err := getTopicMetadataByIDs(topicIDs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "can't get metadata by topic ids")
|
|
||||||
}
|
|
||||||
for _, topic := range topics {
|
|
||||||
topicsMetadata[topic.ID] = &model.TopicMetadata{
|
|
||||||
Id: topic.ID,
|
|
||||||
TopicType: topicType,
|
|
||||||
CollectionType: CollectionTypeRun,
|
|
||||||
TeamId: topic.TeamID,
|
|
||||||
CollectionId: topic.RunID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return topicsMetadata, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeSlice(a, b []string) []string {
|
|
||||||
m := make(map[string]struct{}, len(a)+len(b))
|
|
||||||
for _, elem := range a {
|
|
||||||
m[elem] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, elem := range b {
|
|
||||||
m[elem] = struct{}{}
|
|
||||||
}
|
|
||||||
merged := make([]string, 0, len(m))
|
|
||||||
for key := range m {
|
|
||||||
merged = append(merged, key)
|
|
||||||
}
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package cluster
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// cronPrefix is used to namespace key values created for a job from other key values
|
|
||||||
// created by a plugin.
|
|
||||||
cronPrefix = "cron_"
|
|
||||||
)
|
|
||||||
|
|
||||||
// JobPluginAPI is the plugin API interface required to schedule jobs.
|
|
||||||
type JobPluginAPI interface {
|
|
||||||
MutexPluginAPI
|
|
||||||
KVGet(key string) ([]byte, error)
|
|
||||||
KVDelete(key string) error
|
|
||||||
KVList(page, count int) ([]string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// JobConfig defines the configuration of a scheduled job.
|
|
||||||
type JobConfig struct {
|
|
||||||
// Interval is the period of execution for the job.
|
|
||||||
Interval time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextWaitInterval is a callback computing the next wait interval for a job.
|
|
||||||
type NextWaitInterval func(now time.Time, metadata JobMetadata) time.Duration
|
|
||||||
|
|
||||||
// MakeWaitForInterval creates a function to scheduling a job to run on the given interval relative
|
|
||||||
// to the last finished timestamp.
|
|
||||||
//
|
|
||||||
// For example, if the job first starts at 12:01 PM, and is configured with interval 5 minutes,
|
|
||||||
// it will next run at:
|
|
||||||
//
|
|
||||||
// 12:06, 12:11, 12:16, ...
|
|
||||||
//
|
|
||||||
// If the job has not previously started, it will run immediately.
|
|
||||||
func MakeWaitForInterval(interval time.Duration) NextWaitInterval {
|
|
||||||
if interval == 0 {
|
|
||||||
panic("must specify non-zero ready interval")
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(now time.Time, metadata JobMetadata) time.Duration {
|
|
||||||
sinceLastFinished := now.Sub(metadata.LastFinished)
|
|
||||||
if sinceLastFinished < interval {
|
|
||||||
return interval - sinceLastFinished
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeWaitForRoundedInterval creates a function, scheduling a job to run on the nearest rounded
|
|
||||||
// interval relative to the last finished timestamp.
|
|
||||||
//
|
|
||||||
// For example, if the job first starts at 12:04 PM, and is configured with interval 5 minutes,
|
|
||||||
// and is configured to round to 5 minute intervals, it will next run at:
|
|
||||||
//
|
|
||||||
// 12:05 PM, 12:10 PM, 12:15 PM, ...
|
|
||||||
//
|
|
||||||
// If the job has not previously started, it will run immediately. Note that this wait interval
|
|
||||||
// strategy does not guarantee a minimum interval between runs, only that subsequent runs will be
|
|
||||||
// scheduled on the rounded interval.
|
|
||||||
func MakeWaitForRoundedInterval(interval time.Duration) NextWaitInterval {
|
|
||||||
if interval == 0 {
|
|
||||||
panic("must specify non-zero ready interval")
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(now time.Time, metadata JobMetadata) time.Duration {
|
|
||||||
if metadata.LastFinished.IsZero() {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
target := metadata.LastFinished.Add(interval).Truncate(interval)
|
|
||||||
untilTarget := target.Sub(now)
|
|
||||||
if untilTarget > 0 {
|
|
||||||
return untilTarget
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Job is a scheduled job whose callback function is executed on a configured interval by at most
|
|
||||||
// one plugin instance at a time.
|
|
||||||
//
|
|
||||||
// Use scheduled jobs to perform background activity on a regular interval without having to
|
|
||||||
// explicitly coordinate with other instances of the same plugin that might repeat that effort.
|
|
||||||
type Job struct {
|
|
||||||
pluginAPI JobPluginAPI
|
|
||||||
key string
|
|
||||||
mutex *Mutex
|
|
||||||
nextWaitInterval NextWaitInterval
|
|
||||||
callback func()
|
|
||||||
|
|
||||||
stopOnce sync.Once
|
|
||||||
stop chan bool
|
|
||||||
done chan bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// JobMetadata persists metadata about job execution.
|
|
||||||
type JobMetadata struct {
|
|
||||||
// LastFinished is the last time the job finished anywhere in the cluster.
|
|
||||||
LastFinished time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule creates a scheduled job.
|
|
||||||
func Schedule(pluginAPI JobPluginAPI, key string, nextWaitInterval NextWaitInterval, callback func()) (*Job, error) {
|
|
||||||
key = cronPrefix + key
|
|
||||||
|
|
||||||
mutex, err := NewMutex(pluginAPI, key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to create job mutex")
|
|
||||||
}
|
|
||||||
|
|
||||||
job := &Job{
|
|
||||||
pluginAPI: pluginAPI,
|
|
||||||
key: key,
|
|
||||||
mutex: mutex,
|
|
||||||
nextWaitInterval: nextWaitInterval,
|
|
||||||
callback: callback,
|
|
||||||
stop: make(chan bool),
|
|
||||||
done: make(chan bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
go job.run()
|
|
||||||
|
|
||||||
return job, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readMetadata reads the job execution metadata from the kv store.
|
|
||||||
func (j *Job) readMetadata() (JobMetadata, error) {
|
|
||||||
data, appErr := j.pluginAPI.KVGet(j.key)
|
|
||||||
if appErr != nil {
|
|
||||||
return JobMetadata{}, errors.Wrap(appErr, "failed to read data")
|
|
||||||
}
|
|
||||||
|
|
||||||
if data == nil {
|
|
||||||
return JobMetadata{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata JobMetadata
|
|
||||||
err := json.Unmarshal(data, &metadata)
|
|
||||||
if err != nil {
|
|
||||||
return JobMetadata{}, errors.Wrap(err, "failed to decode data")
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveMetadata writes updated job execution metadata from the kv store.
|
|
||||||
//
|
|
||||||
// It is assumed that the job mutex is held, negating the need to require an atomic write.
|
|
||||||
func (j *Job) saveMetadata(metadata JobMetadata) error {
|
|
||||||
data, err := json.Marshal(metadata)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to marshal data")
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, appErr := j.pluginAPI.KVSetWithOptions(j.key, data, model.PluginKVSetOptions{})
|
|
||||||
if appErr != nil || !ok {
|
|
||||||
return errors.Wrap(appErr, "failed to set data")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// run attempts to run the scheduled job, guaranteeing only one instance is executing concurrently.
|
|
||||||
func (j *Job) run() {
|
|
||||||
defer close(j.done)
|
|
||||||
|
|
||||||
var waitInterval time.Duration
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-j.stop:
|
|
||||||
return
|
|
||||||
case <-time.After(waitInterval):
|
|
||||||
}
|
|
||||||
|
|
||||||
func() {
|
|
||||||
// Acquire the corresponding job lock and hold it throughout execution.
|
|
||||||
j.mutex.Lock()
|
|
||||||
defer j.mutex.Unlock()
|
|
||||||
|
|
||||||
metadata, err := j.readMetadata()
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).WithField("key", j.key).Error("failed to read job metadata")
|
|
||||||
waitInterval = nextWaitInterval(waitInterval, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is it time to run the job?
|
|
||||||
waitInterval = j.nextWaitInterval(time.Now(), metadata)
|
|
||||||
if waitInterval > 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the job
|
|
||||||
j.callback()
|
|
||||||
|
|
||||||
metadata.LastFinished = time.Now()
|
|
||||||
|
|
||||||
err = j.saveMetadata(metadata)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).WithField("key", j.key).Error("failed to write job data")
|
|
||||||
}
|
|
||||||
|
|
||||||
waitInterval = j.nextWaitInterval(time.Now(), metadata)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close terminates a scheduled job, preventing it from being scheduled on this plugin instance.
|
|
||||||
func (j *Job) Close() error {
|
|
||||||
j.stopOnce.Do(func() {
|
|
||||||
close(j.stop)
|
|
||||||
})
|
|
||||||
<-j.done
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package cluster
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"math/rand"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// oncePrefix is used to namespace key values created for a scheduleOnce job
|
|
||||||
oncePrefix = "once_"
|
|
||||||
|
|
||||||
// keysPerPage is the maximum number of keys to retrieve from the db per call
|
|
||||||
keysPerPage = 1000
|
|
||||||
|
|
||||||
// maxNumFails is the maximum number of KVStore read fails or failed attempts to run the
|
|
||||||
// callback until the scheduler cancels a job.
|
|
||||||
maxNumFails = 3
|
|
||||||
|
|
||||||
// waitAfterFail is the amount of time to wait after a failure
|
|
||||||
waitAfterFail = 1 * time.Second
|
|
||||||
|
|
||||||
// pollNewJobsInterval is the amount of time to wait between polling the db for new scheduled jobs
|
|
||||||
pollNewJobsInterval = 5 * time.Minute
|
|
||||||
|
|
||||||
// scheduleOnceJitter is the range of jitter to add to intervals to avoid contention issues
|
|
||||||
scheduleOnceJitter = 100 * time.Millisecond
|
|
||||||
)
|
|
||||||
|
|
||||||
type JobOnceMetadata struct {
|
|
||||||
Key string
|
|
||||||
RunAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type JobOnce struct {
|
|
||||||
pluginAPI JobPluginAPI
|
|
||||||
clusterMutex *Mutex
|
|
||||||
|
|
||||||
// key is the original key. It is prefixed with oncePrefix when used as a key in the KVStore
|
|
||||||
key string
|
|
||||||
runAt time.Time
|
|
||||||
numFails int
|
|
||||||
|
|
||||||
// done signals the job.run go routine to exit
|
|
||||||
done chan bool
|
|
||||||
doneOnce sync.Once
|
|
||||||
|
|
||||||
// join is a join point for the job.run() goroutine to join the calling goroutine (in this case,
|
|
||||||
// the one calling job.Cancel)
|
|
||||||
join chan bool
|
|
||||||
joinOnce sync.Once
|
|
||||||
|
|
||||||
storedCallback *syncedCallback
|
|
||||||
activeJobs *syncedJobs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel terminates a scheduled job, preventing it from being scheduled on this plugin instance.
|
|
||||||
// It also removes the job from the db, preventing it from being run in the future.
|
|
||||||
func (j *JobOnce) Cancel() {
|
|
||||||
j.clusterMutex.Lock()
|
|
||||||
defer j.clusterMutex.Unlock()
|
|
||||||
|
|
||||||
j.cancelWhileHoldingMutex()
|
|
||||||
|
|
||||||
// join the running goroutine
|
|
||||||
j.joinOnce.Do(func() {
|
|
||||||
<-j.join
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func newJobOnce(pluginAPI JobPluginAPI, key string, runAt time.Time, callback *syncedCallback, jobs *syncedJobs) (*JobOnce, error) {
|
|
||||||
mutex, err := NewMutex(pluginAPI, key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to create job mutex")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &JobOnce{
|
|
||||||
pluginAPI: pluginAPI,
|
|
||||||
clusterMutex: mutex,
|
|
||||||
key: key,
|
|
||||||
runAt: runAt,
|
|
||||||
done: make(chan bool),
|
|
||||||
join: make(chan bool),
|
|
||||||
storedCallback: callback,
|
|
||||||
activeJobs: jobs,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *JobOnce) run() {
|
|
||||||
defer close(j.join)
|
|
||||||
|
|
||||||
wait := time.Until(j.runAt)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-j.done:
|
|
||||||
return
|
|
||||||
case <-time.After(wait + addJitter()):
|
|
||||||
}
|
|
||||||
|
|
||||||
func() {
|
|
||||||
// Acquire the cluster mutex while we're trying to do the job
|
|
||||||
j.clusterMutex.Lock()
|
|
||||||
defer j.clusterMutex.Unlock()
|
|
||||||
|
|
||||||
// Check that the job has not been completed
|
|
||||||
metadata, err := readMetadata(j.pluginAPI, j.key)
|
|
||||||
if err != nil {
|
|
||||||
j.numFails++
|
|
||||||
if j.numFails > maxNumFails {
|
|
||||||
j.cancelWhileHoldingMutex()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait a bit of time and try again
|
|
||||||
wait = waitAfterFail
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If key doesn't exist, or if the runAt has changed, the original job has been completed already
|
|
||||||
if metadata == nil || !j.runAt.Equal(metadata.RunAt) {
|
|
||||||
j.cancelWhileHoldingMutex()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
j.executeJob()
|
|
||||||
|
|
||||||
j.cancelWhileHoldingMutex()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *JobOnce) executeJob() {
|
|
||||||
j.storedCallback.mu.Lock()
|
|
||||||
defer j.storedCallback.mu.Unlock()
|
|
||||||
|
|
||||||
j.storedCallback.callback(j.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// readMetadata reads the job's stored metadata. If the caller wishes to make an atomic
|
|
||||||
// read/write, the cluster mutex for job's key should be held.
|
|
||||||
func readMetadata(pluginAPI JobPluginAPI, key string) (*JobOnceMetadata, error) {
|
|
||||||
data, err := pluginAPI.KVGet(oncePrefix + key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to read data")
|
|
||||||
}
|
|
||||||
|
|
||||||
if data == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata JobOnceMetadata
|
|
||||||
if err := json.Unmarshal(data, &metadata); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to decode data")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &metadata, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveMetadata writes the job's metadata to the kvstore. saveMetadata acquires the job's cluster lock.
|
|
||||||
// saveMetadata will not overwrite an existing key.
|
|
||||||
func (j *JobOnce) saveMetadata() error {
|
|
||||||
j.clusterMutex.Lock()
|
|
||||||
defer j.clusterMutex.Unlock()
|
|
||||||
|
|
||||||
metadata := JobOnceMetadata{
|
|
||||||
Key: j.key,
|
|
||||||
RunAt: j.runAt,
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(metadata)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to marshal data")
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err := j.pluginAPI.KVSetWithOptions(oncePrefix+j.key, data, model.PluginKVSetOptions{
|
|
||||||
Atomic: true,
|
|
||||||
OldValue: nil,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return errors.New("failed to set data")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// cancelWhileHoldingMutex assumes the caller holds the job's mutex.
|
|
||||||
func (j *JobOnce) cancelWhileHoldingMutex() {
|
|
||||||
// remove the job from the kv store, if it exists
|
|
||||||
_ = j.pluginAPI.KVDelete(oncePrefix + j.key)
|
|
||||||
|
|
||||||
j.activeJobs.mu.Lock()
|
|
||||||
defer j.activeJobs.mu.Unlock()
|
|
||||||
delete(j.activeJobs.jobs, j.key)
|
|
||||||
|
|
||||||
j.doneOnce.Do(func() {
|
|
||||||
close(j.done)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func addJitter() time.Duration {
|
|
||||||
return time.Duration(rand.Int63n(int64(scheduleOnceJitter)))
|
|
||||||
}
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package cluster
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// syncedCallback uses the mutex to make things predictable for the client: the callback will be
|
|
||||||
// called once at a time (the client does not need to worry about concurrency within the callback)
|
|
||||||
type syncedCallback struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
callback func(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
type syncedJobs struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
jobs map[string]*JobOnce
|
|
||||||
}
|
|
||||||
|
|
||||||
type JobOnceScheduler struct {
|
|
||||||
pluginAPI JobPluginAPI
|
|
||||||
|
|
||||||
startedMu sync.RWMutex
|
|
||||||
started bool
|
|
||||||
|
|
||||||
activeJobs *syncedJobs
|
|
||||||
storedCallback *syncedCallback
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetJobOnceScheduler returns a scheduler which is ready to have its callback set. Repeated
|
|
||||||
// calls will return the same scheduler.
|
|
||||||
func GetJobOnceScheduler(pluginAPI JobPluginAPI) *JobOnceScheduler {
|
|
||||||
return &JobOnceScheduler{
|
|
||||||
pluginAPI: pluginAPI,
|
|
||||||
activeJobs: &syncedJobs{
|
|
||||||
jobs: make(map[string]*JobOnce),
|
|
||||||
},
|
|
||||||
storedCallback: &syncedCallback{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the Scheduler. It finds all previous ScheduleOnce jobs and starts them running, and
|
|
||||||
// fires any jobs that have reached or exceeded their runAt time. Thus, even if a cluster goes down
|
|
||||||
// and is restarted, Start will restart previously scheduled jobs.
|
|
||||||
func (s *JobOnceScheduler) Start() error {
|
|
||||||
s.startedMu.Lock()
|
|
||||||
defer s.startedMu.Unlock()
|
|
||||||
if s.started {
|
|
||||||
return errors.New("scheduler has already been started")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.verifyCallbackExists(); err != nil {
|
|
||||||
return errors.Wrap(err, "callback not found; cannot start scheduler")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.scheduleNewJobsFromDB(); err != nil {
|
|
||||||
return errors.Wrap(err, "could not start JobOnceScheduler due to error")
|
|
||||||
}
|
|
||||||
|
|
||||||
go s.pollForNewScheduledJobs()
|
|
||||||
|
|
||||||
s.started = true
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCallback sets the scheduler's callback. When a job fires, the callback will be called with
|
|
||||||
// the job's id.
|
|
||||||
func (s *JobOnceScheduler) SetCallback(callback func(string)) error {
|
|
||||||
if callback == nil {
|
|
||||||
return errors.New("callback cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.storedCallback.mu.Lock()
|
|
||||||
defer s.storedCallback.mu.Unlock()
|
|
||||||
|
|
||||||
s.storedCallback.callback = callback
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListScheduledJobs returns a list of the jobs in the db that have been scheduled. There is no
|
|
||||||
// guarantee that list is accurate by the time the caller reads the list. E.g., the jobs in the list
|
|
||||||
// may have been run, canceled, or new jobs may have scheduled.
|
|
||||||
func (s *JobOnceScheduler) ListScheduledJobs() ([]JobOnceMetadata, error) {
|
|
||||||
var ret []JobOnceMetadata
|
|
||||||
for i := 0; ; i++ {
|
|
||||||
keys, err := s.pluginAPI.KVList(i, keysPerPage)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error getting KVList")
|
|
||||||
}
|
|
||||||
for _, k := range keys {
|
|
||||||
if strings.HasPrefix(k, oncePrefix) {
|
|
||||||
metadata, err := readMetadata(s.pluginAPI, k[len(oncePrefix):])
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).WithField("key", k).Error("could not retrieve data from plugin kvstore")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if metadata == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = append(ret, *metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(keys) < keysPerPage {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScheduleOnce creates a scheduled job that will run once. When the clock reaches runAt, the
|
|
||||||
// callback will be called with key as the argument.
|
|
||||||
//
|
|
||||||
// If the job key already exists in the db, this will return an error. To reschedule a job, first
|
|
||||||
// cancel the original then schedule it again.
|
|
||||||
func (s *JobOnceScheduler) ScheduleOnce(key string, runAt time.Time) (*JobOnce, error) {
|
|
||||||
s.startedMu.RLock()
|
|
||||||
defer s.startedMu.RUnlock()
|
|
||||||
if !s.started {
|
|
||||||
return nil, errors.New("start the scheduler before adding jobs")
|
|
||||||
}
|
|
||||||
|
|
||||||
job, err := newJobOnce(s.pluginAPI, key, runAt, s.storedCallback, s.activeJobs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "could not create new job")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = job.saveMetadata(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "could not save job metadata")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.runAndTrack(job)
|
|
||||||
|
|
||||||
return job, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel cancels a job by its key. This is useful if the plugin lost the original *JobOnce, or
|
|
||||||
// is stopping a job found in ListScheduledJobs().
|
|
||||||
func (s *JobOnceScheduler) Cancel(key string) {
|
|
||||||
// using an anonymous function because job.Close() below needs access to the activeJobs mutex
|
|
||||||
job := func() *JobOnce {
|
|
||||||
s.activeJobs.mu.RLock()
|
|
||||||
defer s.activeJobs.mu.RUnlock()
|
|
||||||
j, ok := s.activeJobs.jobs[key]
|
|
||||||
if ok {
|
|
||||||
return j
|
|
||||||
}
|
|
||||||
|
|
||||||
// Job wasn't active, so no need to call CancelWhileHoldingMutex (which shuts down the
|
|
||||||
// goroutine). There's a condition where another server in the cluster started the job, and
|
|
||||||
// the current server hasn't polled for it yet. To solve that case, delete it from the db.
|
|
||||||
mutex, err := NewMutex(s.pluginAPI, key)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).WithField("key", key).Error("failed to create job mutex in Cancel")
|
|
||||||
}
|
|
||||||
mutex.Lock()
|
|
||||||
defer mutex.Unlock()
|
|
||||||
|
|
||||||
_ = s.pluginAPI.KVDelete(oncePrefix + key)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
if job != nil {
|
|
||||||
job.Cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *JobOnceScheduler) scheduleNewJobsFromDB() error {
|
|
||||||
scheduled, err := s.ListScheduledJobs()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "could not read scheduled jobs from db")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, m := range scheduled {
|
|
||||||
job, err := newJobOnce(s.pluginAPI, m.Key, m.RunAt, s.storedCallback, s.activeJobs)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).WithField("key", m.Key).Error("could not create new job")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
s.runAndTrack(job)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *JobOnceScheduler) runAndTrack(job *JobOnce) {
|
|
||||||
s.activeJobs.mu.Lock()
|
|
||||||
defer s.activeJobs.mu.Unlock()
|
|
||||||
|
|
||||||
// has this been scheduled already on this server?
|
|
||||||
if _, ok := s.activeJobs.jobs[job.key]; ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go job.run()
|
|
||||||
|
|
||||||
s.activeJobs.jobs[job.key] = job
|
|
||||||
}
|
|
||||||
|
|
||||||
// pollForNewScheduledJobs will only be started once per plugin. It doesn't need to be stopped.
|
|
||||||
func (s *JobOnceScheduler) pollForNewScheduledJobs() {
|
|
||||||
for {
|
|
||||||
<-time.After(pollNewJobsInterval + addJitter())
|
|
||||||
|
|
||||||
if err := s.scheduleNewJobsFromDB(); err != nil {
|
|
||||||
logrus.WithError(err).Error("scheduleOnce poller encountered an error but is still polling")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *JobOnceScheduler) verifyCallbackExists() error {
|
|
||||||
s.storedCallback.mu.Lock()
|
|
||||||
defer s.storedCallback.mu.Unlock()
|
|
||||||
|
|
||||||
if s.storedCallback.callback == nil {
|
|
||||||
return errors.New("set callback before starting the scheduler")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package cluster
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// mutexPrefix is used to namespace key values created for a mutex from other key values
|
|
||||||
// created by a plugin.
|
|
||||||
mutexPrefix = "mutex_"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ttl is the interval after which a locked mutex will expire unless refreshed
|
|
||||||
ttl = time.Second * 15
|
|
||||||
|
|
||||||
// refreshInterval is the interval on which the mutex will be refreshed when locked
|
|
||||||
refreshInterval = ttl / 2
|
|
||||||
)
|
|
||||||
|
|
||||||
// MutexPluginAPI is the plugin API interface required to manage mutexes.
|
|
||||||
type MutexPluginAPI interface {
|
|
||||||
KVSetWithOptions(key string, value []byte, options model.PluginKVSetOptions) (bool, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutex is similar to sync.Mutex, except usable by multiple plugin instances across a cluster.
|
|
||||||
//
|
|
||||||
// Internally, a mutex relies on an atomic key-value set operation as exposed by the Mattermost
|
|
||||||
// plugin API.
|
|
||||||
//
|
|
||||||
// Mutexes with different names are unrelated. Mutexes with the same name from different plugins
|
|
||||||
// are unrelated. Pick a unique name for each mutex your plugin requires.
|
|
||||||
//
|
|
||||||
// A Mutex must not be copied after first use.
|
|
||||||
type Mutex struct {
|
|
||||||
pluginAPI MutexPluginAPI
|
|
||||||
key string
|
|
||||||
|
|
||||||
// lock guards the variables used to manage the refresh task, and is not itself related to
|
|
||||||
// the cluster-wide lock.
|
|
||||||
lock sync.Mutex
|
|
||||||
stopRefresh chan bool
|
|
||||||
refreshDone chan bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMutex creates a mutex with the given key name.
|
|
||||||
//
|
|
||||||
// Panics if key is empty.
|
|
||||||
func NewMutex(pluginAPI MutexPluginAPI, key string) (*Mutex, error) {
|
|
||||||
key, err := makeLockKey(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Mutex{
|
|
||||||
pluginAPI: pluginAPI,
|
|
||||||
key: key,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeLockKey returns the prefixed key used to namespace mutex keys.
|
|
||||||
func makeLockKey(key string) (string, error) {
|
|
||||||
if key == "" {
|
|
||||||
return "", errors.New("must specify valid mutex key")
|
|
||||||
}
|
|
||||||
|
|
||||||
return mutexPrefix + key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// lock makes a single attempt to atomically lock the mutex, returning true only if successful.
|
|
||||||
func (m *Mutex) tryLock() (bool, error) {
|
|
||||||
ok, err := m.pluginAPI.KVSetWithOptions(m.key, []byte{1}, model.PluginKVSetOptions{
|
|
||||||
Atomic: true,
|
|
||||||
OldValue: nil, // No existing key value.
|
|
||||||
ExpireInSeconds: int64(ttl / time.Second),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return false, errors.Wrap(err, "failed to set mutex kv")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// refreshLock rewrites the lock key value with a new expiry, returning true only if successful.
|
|
||||||
func (m *Mutex) refreshLock() error {
|
|
||||||
ok, err := m.pluginAPI.KVSetWithOptions(m.key, []byte{1}, model.PluginKVSetOptions{
|
|
||||||
Atomic: true,
|
|
||||||
OldValue: []byte{1},
|
|
||||||
ExpireInSeconds: int64(ttl / time.Second),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to refresh mutex kv")
|
|
||||||
} else if !ok {
|
|
||||||
return errors.New("unexpectedly failed to refresh mutex kv")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock locks m. If the mutex is already locked by any plugin instance, including the current one,
|
|
||||||
// the calling goroutine blocks until the mutex can be locked.
|
|
||||||
func (m *Mutex) Lock() {
|
|
||||||
_ = m.LockWithContext(context.Background())
|
|
||||||
}
|
|
||||||
|
|
||||||
// LockWithContext locks m unless the context is canceled. If the mutex is already locked by any plugin
|
|
||||||
// instance, including the current one, the calling goroutine blocks until the mutex can be locked,
|
|
||||||
// or the context is canceled.
|
|
||||||
//
|
|
||||||
// The mutex is locked only if a nil error is returned.
|
|
||||||
func (m *Mutex) LockWithContext(ctx context.Context) error {
|
|
||||||
var waitInterval time.Duration
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-time.After(waitInterval):
|
|
||||||
}
|
|
||||||
|
|
||||||
locked, err := m.tryLock()
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).WithField("lock_key", m.key).Error("failed to lock mutex")
|
|
||||||
waitInterval = nextWaitInterval(waitInterval, err)
|
|
||||||
continue
|
|
||||||
} else if !locked {
|
|
||||||
waitInterval = nextWaitInterval(waitInterval, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
stop := make(chan bool)
|
|
||||||
done := make(chan bool)
|
|
||||||
go func() {
|
|
||||||
defer close(done)
|
|
||||||
t := time.NewTicker(refreshInterval)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
err := m.refreshLock()
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).WithField("lock_key", m.key).Error("failed to refresh mutex")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
m.lock.Lock()
|
|
||||||
m.stopRefresh = stop
|
|
||||||
m.refreshDone = done
|
|
||||||
m.lock.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock unlocks m. It is a run-time error if m is not locked on entry to Unlock.
|
|
||||||
//
|
|
||||||
// Just like sync.Mutex, a locked Lock is not associated with a particular goroutine or plugin
|
|
||||||
// instance. It is allowed for one goroutine or plugin instance to lock a Lock and then arrange
|
|
||||||
// for another goroutine or plugin instance to unlock it. In practice, ownership of the lock should
|
|
||||||
// remain within a single plugin instance.
|
|
||||||
func (m *Mutex) Unlock() {
|
|
||||||
m.lock.Lock()
|
|
||||||
if m.stopRefresh == nil {
|
|
||||||
m.lock.Unlock()
|
|
||||||
panic("mutex has not been acquired")
|
|
||||||
}
|
|
||||||
|
|
||||||
close(m.stopRefresh)
|
|
||||||
m.stopRefresh = nil
|
|
||||||
<-m.refreshDone
|
|
||||||
m.lock.Unlock()
|
|
||||||
|
|
||||||
// If an error occurs deleting, the mutex kv will still expire, allowing later retry.
|
|
||||||
_, _ = m.pluginAPI.KVSetWithOptions(m.key, nil, model.PluginKVSetOptions{})
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package cluster
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// minWaitInterval is the minimum amount of time to wait between locking attempts
|
|
||||||
minWaitInterval = 1 * time.Second
|
|
||||||
|
|
||||||
// maxWaitInterval is the maximum amount of time to wait between locking attempts
|
|
||||||
maxWaitInterval = 5 * time.Minute
|
|
||||||
|
|
||||||
// pollWaitInterval is the usual time to wait between unsuccessful locking attempts
|
|
||||||
pollWaitInterval = 1 * time.Second
|
|
||||||
|
|
||||||
// jitterWaitInterval is the amount of jitter to add when waiting to avoid thundering herds
|
|
||||||
jitterWaitInterval = minWaitInterval / 2
|
|
||||||
)
|
|
||||||
|
|
||||||
// nextWaitInterval determines how long to wait until the next lock retry.
|
|
||||||
func nextWaitInterval(lastWaitInterval time.Duration, err error) time.Duration {
|
|
||||||
nextWaitInterval := lastWaitInterval
|
|
||||||
|
|
||||||
if nextWaitInterval <= 0 {
|
|
||||||
nextWaitInterval = minWaitInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
nextWaitInterval *= 2
|
|
||||||
if nextWaitInterval > maxWaitInterval {
|
|
||||||
nextWaitInterval = maxWaitInterval
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nextWaitInterval = pollWaitInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add some jitter to avoid unnecessary collision between competing plugin instances.
|
|
||||||
nextWaitInterval += time.Duration(rand.Int63n(int64(jitterWaitInterval)) - int64(jitterWaitInterval)/2)
|
|
||||||
|
|
||||||
return nextWaitInterval
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package pluginapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
e10 = "E10"
|
|
||||||
e20 = "E20"
|
|
||||||
professional = "professional"
|
|
||||||
enterprise = "enterprise"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsEnterpriseLicensedOrDevelopment returns true when the server is licensed with any Mattermost
|
|
||||||
// Enterprise License, or has `EnableDeveloper` and `EnableTesting` configuration settings
|
|
||||||
// enabled signaling a non-production, developer mode.
|
|
||||||
func IsEnterpriseLicensedOrDevelopment(config *model.Config, license *model.License) bool {
|
|
||||||
if license != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return IsConfiguredForDevelopment(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidSkuShortName returns whether the SKU short name is one of the known strings;
|
|
||||||
// namely: E10 or professional, or E20 or enterprise
|
|
||||||
func isValidSkuShortName(license *model.License) bool {
|
|
||||||
if license == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch license.SkuShortName {
|
|
||||||
case e10, e20, professional, enterprise:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsE10LicensedOrDevelopment returns true when the server is at least licensed with a legacy Mattermost
|
|
||||||
// Enterprise E10 License or a Mattermost Professional License, or has `EnableDeveloper` and
|
|
||||||
// `EnableTesting` configuration settings enabled, signaling a non-production, developer mode.
|
|
||||||
func IsE10LicensedOrDevelopment(config *model.Config, license *model.License) bool {
|
|
||||||
if license != nil &&
|
|
||||||
(license.SkuShortName == e10 || license.SkuShortName == professional ||
|
|
||||||
license.SkuShortName == e20 || license.SkuShortName == enterprise) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isValidSkuShortName(license) {
|
|
||||||
// As a fallback for licenses whose SKU short name is unknown, make a best effort to try
|
|
||||||
// and use the presence of a known E10/Professional feature as a check to determine licensing.
|
|
||||||
if license != nil &&
|
|
||||||
license.Features != nil &&
|
|
||||||
license.Features.LDAP != nil &&
|
|
||||||
*license.Features.LDAP {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return IsConfiguredForDevelopment(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsE20LicensedOrDevelopment returns true when the server is licensed with a legacy Mattermost
|
|
||||||
// Enterprise E20 License or a Mattermost Enterprise License, or has `EnableDeveloper` and
|
|
||||||
// `EnableTesting` configuration settings enabled, signaling a non-production, developer mode.
|
|
||||||
func IsE20LicensedOrDevelopment(config *model.Config, license *model.License) bool {
|
|
||||||
if license != nil && (license.SkuShortName == e20 || license.SkuShortName == enterprise) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isValidSkuShortName(license) {
|
|
||||||
// As a fallback for licenses whose SKU short name is unknown, make a best effort to try
|
|
||||||
// and use the presence of a known E20/Enterprise feature as a check to determine licensing.
|
|
||||||
if license != nil &&
|
|
||||||
license.Features != nil &&
|
|
||||||
license.Features.FutureFeatures != nil &&
|
|
||||||
*license.Features.FutureFeatures {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return IsConfiguredForDevelopment(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsConfiguredForDevelopment returns true when the server has `EnableDeveloper` and `EnableTesting`
|
|
||||||
// configuration settings enabled, signaling a non-production, developer mode.
|
|
||||||
func IsConfiguredForDevelopment(config *model.Config) bool {
|
|
||||||
if config != nil &&
|
|
||||||
config.ServiceSettings.EnableTesting != nil &&
|
|
||||||
*config.ServiceSettings.EnableTesting &&
|
|
||||||
config.ServiceSettings.EnableDeveloper != nil &&
|
|
||||||
*config.ServiceSettings.EnableDeveloper {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsCloud returns true when the server is on cloud, and false otherwise.
|
|
||||||
func IsCloud(license *model.License) bool {
|
|
||||||
if license == nil || license.Features == nil || license.Features.Cloud == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return *license.Features.Cloud
|
|
||||||
}
|
|
||||||
3
server/playbooks/server/.gitignore
поставляемый
3
server/playbooks/server/.gitignore
поставляемый
@@ -1,3 +0,0 @@
|
|||||||
coverage.txt
|
|
||||||
dist
|
|
||||||
data
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ActionsHandler struct {
|
|
||||||
*ErrorHandler
|
|
||||||
channelActionsService app.ChannelActionService
|
|
||||||
api playbooks.ServicesAPI
|
|
||||||
permissions *app.PermissionsService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewActionsHandler(router *mux.Router, channelActionsService app.ChannelActionService, api playbooks.ServicesAPI, permissions *app.PermissionsService) *ActionsHandler {
|
|
||||||
handler := &ActionsHandler{
|
|
||||||
ErrorHandler: &ErrorHandler{},
|
|
||||||
channelActionsService: channelActionsService,
|
|
||||||
api: api,
|
|
||||||
permissions: permissions,
|
|
||||||
}
|
|
||||||
|
|
||||||
actionsRouter := router.PathPrefix("/actions").Subrouter()
|
|
||||||
|
|
||||||
channelsActionsRouter := actionsRouter.PathPrefix("/channels").Subrouter()
|
|
||||||
channelActionsRouter := channelsActionsRouter.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter()
|
|
||||||
channelActionsRouter.HandleFunc("", withContext(handler.createChannelAction)).Methods(http.MethodPost)
|
|
||||||
channelActionsRouter.HandleFunc("", withContext(handler.getChannelActions)).Methods(http.MethodGet)
|
|
||||||
channelActionsRouter.HandleFunc("/check-and-send-message-on-join", withContext(handler.checkAndSendMessageOnJoin)).Methods(http.MethodGet)
|
|
||||||
|
|
||||||
channelActionRouter := channelActionsRouter.PathPrefix("/{action_id:[A-Za-z0-9]+}").Subrouter()
|
|
||||||
channelActionRouter.HandleFunc("", withContext(handler.updateChannelAction)).Methods(http.MethodPut)
|
|
||||||
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ActionsHandler) createChannelAction(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
channelID := vars["channel_id"]
|
|
||||||
|
|
||||||
if !a.PermissionsCheck(w, c.logger, a.permissions.ChannelActionCreate(userID, channelID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var channelAction app.GenericChannelAction
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&channelAction); err != nil {
|
|
||||||
a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to parse action", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that the channel ID in both the URL and the body of the request are the same;
|
|
||||||
// otherwise the permission check done above no longer makes sense
|
|
||||||
if channelAction.ChannelID != channelID {
|
|
||||||
a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "channel ID in request body must match channel ID in URL", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the action type and payload
|
|
||||||
if err := a.channelActionsService.Validate(channelAction); err != nil {
|
|
||||||
a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid action", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := a.channelActionsService.Create(channelAction)
|
|
||||||
if err != nil {
|
|
||||||
a.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to create action", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result := struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}{
|
|
||||||
ID: id,
|
|
||||||
}
|
|
||||||
w.Header().Add("Location", makeAPIURL(a.api, "actions/channel/%s/%s", channelAction.ChannelID, id))
|
|
||||||
|
|
||||||
ReturnJSON(w, &result, http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidTrigger(trigger string) bool {
|
|
||||||
if trigger == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, elem := range app.ValidTriggerTypes {
|
|
||||||
if trigger == string(elem) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidAction(action string) bool {
|
|
||||||
if action == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, elem := range app.ValidActionTypes {
|
|
||||||
if action == string(elem) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseGetChannelActionsOptions(query url.Values) (*app.GetChannelActionOptions, error) {
|
|
||||||
actionTypeStr := query.Get("action_type")
|
|
||||||
triggerTypeStr := query.Get("trigger_type")
|
|
||||||
|
|
||||||
if !isValidAction(actionTypeStr) {
|
|
||||||
return nil, fmt.Errorf("action_type %q not recognized; valid values are %v", actionTypeStr, app.ValidActionTypes)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isValidTrigger(triggerTypeStr) {
|
|
||||||
return nil, fmt.Errorf("trigger_type %q not recognized; valid values are %v", triggerTypeStr, app.ValidTriggerTypes)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &app.GetChannelActionOptions{
|
|
||||||
ActionType: app.ActionType(actionTypeStr),
|
|
||||||
TriggerType: app.TriggerType(triggerTypeStr),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ActionsHandler) getChannelActions(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
channelID := vars["channel_id"]
|
|
||||||
|
|
||||||
if !a.PermissionsCheck(w, c.logger, a.permissions.ChannelActionView(userID, channelID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
options, err := parseGetChannelActionsOptions(r.URL.Query())
|
|
||||||
if err != nil {
|
|
||||||
a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, errors.Wrapf(err, "bad options").Error(), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
actions, err := a.channelActionsService.GetChannelActions(channelID, *options)
|
|
||||||
if err != nil {
|
|
||||||
a.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, fmt.Sprintf("unable to retrieve actions for channel %s", channelID), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ReturnJSON(w, &actions, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkAndSendMessageOnJoin handles the GET /actions/channels/{channel_id}/check_and_send_message_on_join endpoint.
|
|
||||||
func (a *ActionsHandler) checkAndSendMessageOnJoin(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
channelID := vars["channel_id"]
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
if !a.PermissionsCheck(w, c.logger, a.permissions.ChannelActionView(userID, channelID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
hasViewed := a.channelActionsService.CheckAndSendMessageOnJoin(userID, channelID)
|
|
||||||
ReturnJSON(w, map[string]interface{}{"viewed": hasViewed}, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ActionsHandler) updateChannelAction(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
channelID := vars["channel_id"]
|
|
||||||
|
|
||||||
if !a.PermissionsCheck(w, c.logger, a.permissions.ChannelActionUpdate(userID, channelID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var newChannelAction app.GenericChannelAction
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&newChannelAction); err != nil {
|
|
||||||
a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to parse action", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that the channel ID in both the URL and the body of the request are the same;
|
|
||||||
// otherwise the permission check done above no longer makes sense
|
|
||||||
if newChannelAction.ChannelID != channelID {
|
|
||||||
a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "channel ID in request body must match channel ID in URL", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the new action type and payload
|
|
||||||
if err := a.channelActionsService.Validate(newChannelAction); err != nil {
|
|
||||||
a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid action", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := a.channelActionsService.Update(newChannelAction, userID)
|
|
||||||
if err != nil {
|
|
||||||
a.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, fmt.Sprintf("unable to update action with ID %q", newChannelAction.ID), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MaxRequestSize is the size limit for any incoming request
|
|
||||||
// The default limit set by mattermost-server is the configured max file size, and
|
|
||||||
// it sometimes isn't small enough to prevent some scenarios.
|
|
||||||
//
|
|
||||||
// This is important to prevent huge payloads from being sent
|
|
||||||
// that could end in a bigger problem.
|
|
||||||
//
|
|
||||||
// If an endpoint needs a smaller limit than this one, it could be solved by adding their
|
|
||||||
// own limit BEFORE reading the request body `r.Body = http.MaxBytesReader(w, r.Body, MaxRequestSize)`
|
|
||||||
const MaxRequestSize = 5 * 1024 * 1024 // 5MB
|
|
||||||
|
|
||||||
// Handler Root API handler.
|
|
||||||
type Handler struct {
|
|
||||||
*ErrorHandler
|
|
||||||
APIRouter *mux.Router
|
|
||||||
root *mux.Router
|
|
||||||
config config.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHandler constructs a new handler.
|
|
||||||
func NewHandler(config config.Service) *Handler {
|
|
||||||
handler := &Handler{
|
|
||||||
ErrorHandler: &ErrorHandler{},
|
|
||||||
config: config,
|
|
||||||
}
|
|
||||||
|
|
||||||
root := mux.NewRouter()
|
|
||||||
api := root.PathPrefix("/api/v0").Subrouter()
|
|
||||||
api.Use(LogRequest)
|
|
||||||
api.Use(MattermostAuthorizationRequired)
|
|
||||||
|
|
||||||
api.Handle("{anything:.*}", http.NotFoundHandler())
|
|
||||||
api.NotFoundHandler = http.NotFoundHandler()
|
|
||||||
|
|
||||||
handler.APIRouter = api
|
|
||||||
handler.root = root
|
|
||||||
handler.config = config
|
|
||||||
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, MaxRequestSize)
|
|
||||||
h.root.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleResponseWithCode logs the internal error and sends the public facing error
|
|
||||||
// message as JSON in a response with the provided code.
|
|
||||||
func handleResponseWithCode(w http.ResponseWriter, code int, publicMsg string) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(code)
|
|
||||||
|
|
||||||
responseMsg, _ := json.Marshal(struct {
|
|
||||||
Error string `json:"error"` // A public facing message providing details about the error.
|
|
||||||
}{
|
|
||||||
Error: publicMsg,
|
|
||||||
})
|
|
||||||
_, _ = w.Write(responseMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleErrorWithCode logs the internal error and sends the public facing error
|
|
||||||
// message as JSON in a response with the provided code.
|
|
||||||
func HandleErrorWithCode(logger logrus.FieldLogger, w http.ResponseWriter, code int, publicErrorMsg string, internalErr error) {
|
|
||||||
if internalErr != nil {
|
|
||||||
logger = logger.WithError(internalErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if code >= http.StatusInternalServerError {
|
|
||||||
logger.Error(publicErrorMsg)
|
|
||||||
} else {
|
|
||||||
logger.Warn(publicErrorMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleResponseWithCode(w, code, publicErrorMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReturnJSON writes the given pointerToObject as json with the provided httpStatus
|
|
||||||
func ReturnJSON(w http.ResponseWriter, pointerToObject interface{}, httpStatus int) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(httpStatus)
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(pointerToObject); err != nil {
|
|
||||||
logrus.WithError(err).Warn("Unable to write to http.ResponseWriter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MattermostAuthorizationRequired checks if request is authorized.
|
|
||||||
func MattermostAuthorizationRequired(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Header.Get("Mattermost-User-Id")
|
|
||||||
if userID != "" {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,226 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/bot"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/config"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BotHandler struct {
|
|
||||||
*ErrorHandler
|
|
||||||
api playbooks.ServicesAPI
|
|
||||||
poster bot.Poster
|
|
||||||
config config.Service
|
|
||||||
playbookRunService app.PlaybookRunService
|
|
||||||
userInfoStore app.UserInfoStore
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBotHandler(router *mux.Router, api playbooks.ServicesAPI, poster bot.Poster, config config.Service, playbookRunService app.PlaybookRunService, userInfoStore app.UserInfoStore) *BotHandler {
|
|
||||||
handler := &BotHandler{
|
|
||||||
ErrorHandler: &ErrorHandler{},
|
|
||||||
api: api,
|
|
||||||
poster: poster,
|
|
||||||
config: config,
|
|
||||||
playbookRunService: playbookRunService,
|
|
||||||
userInfoStore: userInfoStore,
|
|
||||||
}
|
|
||||||
|
|
||||||
botRouter := router.PathPrefix("/bot").Subrouter()
|
|
||||||
|
|
||||||
notifyAdminsRouter := botRouter.PathPrefix("/notify-admins").Subrouter()
|
|
||||||
notifyAdminsRouter.HandleFunc("", withContext(handler.notifyAdmins)).Methods(http.MethodPost)
|
|
||||||
notifyAdminsRouter.HandleFunc("/button-start-trial", withContext(handler.startTrial)).Methods(http.MethodPost)
|
|
||||||
|
|
||||||
botRouter.HandleFunc("/connect", withContext(handler.connect)).Methods(http.MethodGet)
|
|
||||||
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
type messagePayload struct {
|
|
||||||
MessageType string `json:"message_type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *BotHandler) notifyAdmins(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
var payload messagePayload
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode message", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.poster.NotifyAdmins(payload.MessageType, userID, !h.api.IsEnterpriseReady()); err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CanStartTrialLicense(userID string, api playbooks.ServicesAPI) error {
|
|
||||||
if !api.HasPermissionTo(userID, model.PermissionManageLicenseInformation) {
|
|
||||||
return errors.Wrap(app.ErrNoPermissions, "no permission to manage license information")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *BotHandler) startTrial(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
if err := CanStartTrialLicense(userID, h.api); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "no permission to start a trial license", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestData *model.PostActionIntegrationRequest
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&requestData)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to parse json", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if requestData == nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "missing request data", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
users, ok := requestData.Context["users"].(float64)
|
|
||||||
if !ok {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "malformed context: users is not a number", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
termsAccepted, ok := requestData.Context["termsAccepted"].(bool)
|
|
||||||
if !ok {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "malformed context: termsAccepted is not a boolean", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
receiveEmailsAccepted, ok := requestData.Context["receiveEmailsAccepted"].(bool)
|
|
||||||
if !ok {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "malformed context: receiveEmailsAccepted is not a boolean", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
originalPost, err := h.api.GetPost(requestData.PostId)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modify the button text while the license is downloading
|
|
||||||
originalAttachments := originalPost.Attachments()
|
|
||||||
outer:
|
|
||||||
for _, attachment := range originalAttachments {
|
|
||||||
for _, action := range attachment.Actions {
|
|
||||||
if action.Id == "message" {
|
|
||||||
action.Name = "Requesting trial..."
|
|
||||||
break outer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
model.ParseSlackAttachment(originalPost, originalAttachments)
|
|
||||||
_, _ = h.api.UpdatePost(originalPost)
|
|
||||||
|
|
||||||
post := &model.Post{
|
|
||||||
Id: requestData.PostId,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.api.RequestTrialLicense(requestData.UserId, int(users), termsAccepted, receiveEmailsAccepted); err != nil {
|
|
||||||
post.Message = "Trial license could not be retrieved. Visit [https://mattermost.com/trial/](https://mattermost.com/trial/) to request a license."
|
|
||||||
|
|
||||||
if _, postErr := h.api.UpdatePost(post); postErr != nil {
|
|
||||||
logrus.WithError(postErr).WithField("post_id", post.Id).Error("unable to edit the admin notification post")
|
|
||||||
}
|
|
||||||
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to request the trial license", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
post.Message = "Thank you!"
|
|
||||||
attachments := []*model.SlackAttachment{
|
|
||||||
{
|
|
||||||
Title: "You’re currently on a free trial of Mattermost Enterprise.",
|
|
||||||
Text: "Your free trial will expire in **30 days**. Visit our Customer Portal to purchase a license to continue using commercial edition features after your trial ends.\n[Purchase a license](https://customers.mattermost.com/signup)\n[Contact sales](https://mattermost.com/contact-us/)",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
model.ParseSlackAttachment(post, attachments)
|
|
||||||
|
|
||||||
if _, err := h.api.UpdatePost(post); err != nil {
|
|
||||||
logrus.WithError(err).WithField("post_id", post.Id).Error("unable to edit the admin notification post")
|
|
||||||
}
|
|
||||||
|
|
||||||
ReturnJSON(w, post, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
type DigestSenderParams struct {
|
|
||||||
isWeekly bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// connect handles the GET /bot/connect endpoint (a notification sent when the client wakes up or reconnects)
|
|
||||||
func (h *BotHandler) connect(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
info, err := h.userInfoStore.Get(userID)
|
|
||||||
if errors.Is(err, app.ErrNotFound) {
|
|
||||||
info = app.UserInfo{
|
|
||||||
ID: userID,
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var timezone *time.Location
|
|
||||||
offset, _ := strconv.Atoi(r.Header.Get("X-Timezone-Offset"))
|
|
||||||
timezone = time.FixedZone("local", offset*60*60)
|
|
||||||
|
|
||||||
sendRegularDigest := h.createDigestSender(c, w, userID, &info)
|
|
||||||
|
|
||||||
// we want to first try a weekly digest
|
|
||||||
// if we have already sent it this week, try with a daily one
|
|
||||||
currentTime := time.UnixMilli(model.GetMillis()).In(timezone)
|
|
||||||
if app.ShouldSendWeeklyDigestMessage(info, timezone, currentTime) {
|
|
||||||
sendRegularDigest(DigestSenderParams{isWeekly: true})
|
|
||||||
} else if app.ShouldSendDailyDigestMessage(info, timezone, currentTime) {
|
|
||||||
sendRegularDigest(DigestSenderParams{isWeekly: false})
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *BotHandler) createDigestSender(c *Context, w http.ResponseWriter, userID string, userInfo *app.UserInfo) func(DigestSenderParams) {
|
|
||||||
return func(params DigestSenderParams) {
|
|
||||||
now := model.GetMillis()
|
|
||||||
// record that we're sending a DM now (this will prevent us trying over and over on every
|
|
||||||
// response if there's a failure later)
|
|
||||||
userInfo.LastDailyTodoDMAt = now
|
|
||||||
if err := h.userInfoStore.Upsert(*userInfo); err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
regulartity := "daily"
|
|
||||||
if params.isWeekly {
|
|
||||||
regulartity = "weekly"
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.playbookRunService.DMTodoDigestToUser(userID, false, params.isWeekly); err != nil {
|
|
||||||
h.HandleError(w, c.logger, errors.Wrapf(err, "failed to send '%s' DMTodoDigest to userID '%s'", regulartity, userID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,355 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
const maxItemsInRunsAndPlaybooksCategory = 1000
|
|
||||||
|
|
||||||
type CategoryHandler struct {
|
|
||||||
*ErrorHandler
|
|
||||||
api playbooks.ServicesAPI
|
|
||||||
categoryService app.CategoryService
|
|
||||||
playbookService app.PlaybookService
|
|
||||||
playbookRunService app.PlaybookRunService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCategoryHandler(router *mux.Router, api playbooks.ServicesAPI, categoryService app.CategoryService, playbookService app.PlaybookService, playbookRunService app.PlaybookRunService) *CategoryHandler {
|
|
||||||
handler := &CategoryHandler{
|
|
||||||
ErrorHandler: &ErrorHandler{},
|
|
||||||
api: api,
|
|
||||||
categoryService: categoryService,
|
|
||||||
playbookService: playbookService,
|
|
||||||
playbookRunService: playbookRunService,
|
|
||||||
}
|
|
||||||
|
|
||||||
categoriesRouter := router.PathPrefix("/my_categories").Subrouter()
|
|
||||||
categoriesRouter.HandleFunc("", withContext(handler.getMyCategories)).Methods(http.MethodGet)
|
|
||||||
categoriesRouter.HandleFunc("", withContext(handler.createMyCategory)).Methods(http.MethodPost)
|
|
||||||
categoriesRouter.HandleFunc("/favorites", withContext(handler.isFavorite)).Methods(http.MethodGet)
|
|
||||||
|
|
||||||
categoryRouter := categoriesRouter.PathPrefix("/{id:[A-Za-z0-9]+}").Subrouter()
|
|
||||||
categoryRouter.HandleFunc("", withContext(handler.updateMyCategory)).Methods(http.MethodPut)
|
|
||||||
categoryRouter.HandleFunc("", withContext(handler.deleteMyCategory)).Methods(http.MethodDelete)
|
|
||||||
categoryRouter.HandleFunc("/collapse", withContext(handler.collapseMyCategory)).Methods(http.MethodPut)
|
|
||||||
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *CategoryHandler) getMyCategories(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
params := r.URL.Query()
|
|
||||||
teamID := params.Get("team_id")
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
customCategories, err := h.categoryService.GetCategories(teamID, userID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
filteredCustomCategories := filterEmptyCategories(customCategories)
|
|
||||||
|
|
||||||
runsCategory, err := h.getRunsCategory(teamID, userID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
filteredRuns := filterDuplicatesFromCategory(runsCategory, filteredCustomCategories)
|
|
||||||
allCategories := append([]app.Category{}, customCategories...)
|
|
||||||
allCategories = append(allCategories, filteredRuns)
|
|
||||||
|
|
||||||
playbooksCategory, err := h.getPlaybooksCategory(teamID, userID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
filteredPlaybooks := filterDuplicatesFromCategory(playbooksCategory, filteredCustomCategories)
|
|
||||||
allCategories = append(allCategories, filteredPlaybooks)
|
|
||||||
|
|
||||||
ReturnJSON(w, allCategories, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *CategoryHandler) createMyCategory(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
var category app.Category
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&category); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode category", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if category.ID != "" {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Category given already has ID", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// user can only create category for themselves
|
|
||||||
if category.UserID != userID {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, fmt.Sprintf("userID %s and category userID %s mismatch", userID, category.UserID), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createdCategory, err := h.categoryService.Create(category)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ReturnJSON(w, createdCategory, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *CategoryHandler) updateMyCategory(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
categoryID := vars["id"]
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
var category app.Category
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&category); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode category", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if categoryID != category.ID {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "categoryID mismatch in patch and body", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// user can only update category for themselves
|
|
||||||
if category.UserID != userID {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "user ID mismatch in session and category", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify if category belongs to the user
|
|
||||||
existingCategory, err := h.categoryService.Get(category.ID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Can't get category", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if existingCategory.DeleteAt != 0 {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Category deleted", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if existingCategory.UserID != category.UserID {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "UserID mismatch", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.categoryService.Update(category); err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *CategoryHandler) collapseMyCategory(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
categoryID := vars["id"]
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
var collapsed bool
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&collapsed); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode collapsed", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
existingCategory, err := h.categoryService.Get(categoryID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Can't get category", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if existingCategory.DeleteAt != 0 {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Category deleted", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify if category belongs to the user
|
|
||||||
if existingCategory.UserID != userID {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "UserID mismatch", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if existingCategory.Collapsed == collapsed {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
patchedCategory := existingCategory
|
|
||||||
patchedCategory.Collapsed = collapsed
|
|
||||||
patchedCategory.UpdateAt = model.GetMillis()
|
|
||||||
|
|
||||||
if err := h.categoryService.Update(patchedCategory); err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *CategoryHandler) deleteMyCategory(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
categoryID := vars["id"]
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
existingCategory, err := h.categoryService.Get(categoryID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Can't get category", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// category is already deleted. This avoids
|
|
||||||
// overriding the original deleted at timestamp
|
|
||||||
if existingCategory.DeleteAt != 0 {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Category deleted", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify if category belongs to the user
|
|
||||||
if existingCategory.UserID != userID {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "UserID mismatch", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.categoryService.Delete(categoryID); err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *CategoryHandler) isFavorite(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
params := r.URL.Query()
|
|
||||||
teamID := params.Get("team_id")
|
|
||||||
itemID := params.Get("item_id")
|
|
||||||
itemType := params.Get("type")
|
|
||||||
convertedItemType, err := app.StringToItemType(itemType)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isFavorite, err := h.categoryService.IsItemFavorite(app.CategoryItem{ItemID: itemID, Type: convertedItemType}, teamID, userID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ReturnJSON(w, isFavorite, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *CategoryHandler) getRunsCategory(teamID, userID string) (app.Category, error) {
|
|
||||||
runs, err := h.playbookRunService.GetPlaybookRuns(
|
|
||||||
app.RequesterInfo{
|
|
||||||
UserID: userID,
|
|
||||||
TeamID: teamID,
|
|
||||||
},
|
|
||||||
app.PlaybookRunFilterOptions{
|
|
||||||
TeamID: teamID,
|
|
||||||
ParticipantOrFollowerID: userID,
|
|
||||||
Statuses: []string{app.StatusInProgress},
|
|
||||||
Types: []string{app.RunTypePlaybook}, // only playbook runs can be viewed in Playbook product
|
|
||||||
Page: 0,
|
|
||||||
PerPage: maxItemsInRunsAndPlaybooksCategory,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return app.Category{}, errors.Wrapf(err, "can't get playbook runs")
|
|
||||||
}
|
|
||||||
|
|
||||||
runCategoryItems := []app.CategoryItem{}
|
|
||||||
for _, run := range runs.Items {
|
|
||||||
runCategoryItems = append(runCategoryItems, app.CategoryItem{
|
|
||||||
ItemID: run.ID,
|
|
||||||
Type: app.RunItemType,
|
|
||||||
Name: run.Name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
runCategory := app.Category{
|
|
||||||
ID: "runsCategory",
|
|
||||||
Name: "Runs",
|
|
||||||
TeamID: teamID,
|
|
||||||
UserID: userID,
|
|
||||||
Collapsed: false,
|
|
||||||
Items: runCategoryItems,
|
|
||||||
}
|
|
||||||
return runCategory, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *CategoryHandler) getPlaybooksCategory(teamID, userID string) (app.Category, error) {
|
|
||||||
playbooks, err := h.playbookService.GetPlaybooksForTeam(
|
|
||||||
app.RequesterInfo{
|
|
||||||
TeamID: teamID,
|
|
||||||
UserID: userID,
|
|
||||||
},
|
|
||||||
teamID,
|
|
||||||
app.PlaybookFilterOptions{
|
|
||||||
Page: 0,
|
|
||||||
PerPage: maxItemsInRunsAndPlaybooksCategory,
|
|
||||||
WithMembershipOnly: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return app.Category{}, errors.Wrap(err, "can't get playbooks for team")
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookCategoryItems := []app.CategoryItem{}
|
|
||||||
for _, playbook := range playbooks.Items {
|
|
||||||
playbookCategoryItems = append(playbookCategoryItems, app.CategoryItem{
|
|
||||||
ItemID: playbook.ID,
|
|
||||||
Type: app.PlaybookItemType,
|
|
||||||
Name: playbook.Title,
|
|
||||||
Public: playbook.Public,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookCategory := app.Category{
|
|
||||||
ID: "playbooksCategory",
|
|
||||||
Name: "Playbooks",
|
|
||||||
TeamID: teamID,
|
|
||||||
UserID: userID,
|
|
||||||
Collapsed: false,
|
|
||||||
Items: playbookCategoryItems,
|
|
||||||
}
|
|
||||||
return playbookCategory, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func categoriesContainItem(categories []app.Category, item app.CategoryItem) bool {
|
|
||||||
for _, category := range categories {
|
|
||||||
if category.ContainsItem(item) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterDuplicatesFromCategory(category app.Category, categories []app.Category) app.Category {
|
|
||||||
newItems := []app.CategoryItem{}
|
|
||||||
for _, item := range category.Items {
|
|
||||||
if !categoriesContainItem(categories, item) {
|
|
||||||
newItems = append(newItems, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
category.Items = newItems
|
|
||||||
return category
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterEmptyCategories(categories []app.Category) []app.Category {
|
|
||||||
newCategories := []app.Category{}
|
|
||||||
for _, category := range categories {
|
|
||||||
if len(category.Items) > 0 {
|
|
||||||
newCategories = append(newCategories, category)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newCategories
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// requestIDContextKeyType ensures requestIDContextKey can never collide with another context key
|
|
||||||
// having the same value.
|
|
||||||
type requestIDContextKeyType string
|
|
||||||
|
|
||||||
// requestIDContextKey is the key for the incoming requestID.
|
|
||||||
var requestIDContextKey = requestIDContextKeyType("requestID")
|
|
||||||
|
|
||||||
// getLogger builds a logger with the requestID attached to the given request.
|
|
||||||
func getLogger(r *http.Request) logrus.FieldLogger {
|
|
||||||
var logger logrus.FieldLogger = logrus.StandardLogger()
|
|
||||||
|
|
||||||
requestID, ok := r.Context().Value(requestIDContextKey).(string)
|
|
||||||
if ok {
|
|
||||||
logger = logger.WithField("request_id", requestID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
type Context struct {
|
|
||||||
logger logrus.FieldLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
// withContext passes a logger to http handler functions.
|
|
||||||
func withContext(handler func(c *Context, w http.ResponseWriter, r *http.Request)) func(http.ResponseWriter, *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := getLogger(r)
|
|
||||||
handler(&Context{logger}, w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ErrorHandler struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleError logs the internal error and sends a generic error as JSON in a 500 response.
|
|
||||||
func (h *ErrorHandler) HandleError(w http.ResponseWriter, logger logrus.FieldLogger, internalErr error) {
|
|
||||||
h.HandleErrorWithCode(w, logger, http.StatusInternalServerError, "An internal error has occurred. Check app server logs for details.", internalErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleErrorWithCode logs the internal error and sends the public facing error
|
|
||||||
// message as JSON in a response with the provided code.
|
|
||||||
func (h *ErrorHandler) HandleErrorWithCode(w http.ResponseWriter, logger logrus.FieldLogger, code int, publicErrorMsg string, internalErr error) {
|
|
||||||
HandleErrorWithCode(logger, w, code, publicErrorMsg, internalErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PermissionsCheck handles the output of a permission check
|
|
||||||
// Automatically does the proper error handling.
|
|
||||||
// Returns true if the check passed and false on failure. Correct use is: if !h.PermissionsCheck(w, check) { return }
|
|
||||||
func (h *ErrorHandler) PermissionsCheck(w http.ResponseWriter, logger logrus.FieldLogger, checkOutput error) bool {
|
|
||||||
if checkOutput != nil {
|
|
||||||
h.HandleErrorWithCode(w, logger, http.StatusForbidden, "Not authorized", checkOutput)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/graph-gophers/dataloader/v7"
|
|
||||||
)
|
|
||||||
|
|
||||||
const loaderBatchCapacity = 200
|
|
||||||
|
|
||||||
func populateResultWithError[K any](err error, result []*dataloader.Result[K]) []*dataloader.Result[K] {
|
|
||||||
for i := range result {
|
|
||||||
result[i] = &dataloader.Result[K]{Error: err}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
_ "embed"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/graph-gophers/dataloader/v7"
|
|
||||||
graphql "github.com/graph-gophers/graphql-go"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/config"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GraphQLHandler struct {
|
|
||||||
*ErrorHandler
|
|
||||||
playbookService app.PlaybookService
|
|
||||||
playbookRunService app.PlaybookRunService
|
|
||||||
categoryService app.CategoryService
|
|
||||||
api playbooks.ServicesAPI
|
|
||||||
config config.Service
|
|
||||||
permissions *app.PermissionsService
|
|
||||||
playbookStore app.PlaybookStore
|
|
||||||
licenceChecker app.LicenseChecker
|
|
||||||
|
|
||||||
schema *graphql.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:embed schema.graphqls
|
|
||||||
var SchemaFile string
|
|
||||||
|
|
||||||
func NewGraphQLHandler(
|
|
||||||
router *mux.Router,
|
|
||||||
playbookService app.PlaybookService,
|
|
||||||
playbookRunService app.PlaybookRunService,
|
|
||||||
categoryService app.CategoryService,
|
|
||||||
api playbooks.ServicesAPI,
|
|
||||||
configService config.Service,
|
|
||||||
permissions *app.PermissionsService,
|
|
||||||
playbookStore app.PlaybookStore,
|
|
||||||
licenceChecker app.LicenseChecker,
|
|
||||||
) *GraphQLHandler {
|
|
||||||
handler := &GraphQLHandler{
|
|
||||||
ErrorHandler: &ErrorHandler{},
|
|
||||||
playbookService: playbookService,
|
|
||||||
playbookRunService: playbookRunService,
|
|
||||||
categoryService: categoryService,
|
|
||||||
api: api,
|
|
||||||
config: configService,
|
|
||||||
permissions: permissions,
|
|
||||||
playbookStore: playbookStore,
|
|
||||||
licenceChecker: licenceChecker,
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := []graphql.SchemaOpt{
|
|
||||||
graphql.UseFieldResolvers(),
|
|
||||||
graphql.MaxParallelism(5),
|
|
||||||
}
|
|
||||||
|
|
||||||
if !configService.IsConfiguredForDevelopmentAndTesting() {
|
|
||||||
opts = append(opts,
|
|
||||||
graphql.MaxDepth(8),
|
|
||||||
graphql.DisableIntrospection(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
root := &RootResolver{}
|
|
||||||
var err error
|
|
||||||
handler.schema, err = graphql.ParseSchema(SchemaFile, root, opts...)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Error("unable to parse graphql schema")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
router.HandleFunc("/query", withContext(graphiQL)).Methods("GET")
|
|
||||||
router.HandleFunc("/query", withContext(handler.graphQL)).Methods("POST")
|
|
||||||
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
type ctxKey struct{}
|
|
||||||
|
|
||||||
type GraphQLContext struct {
|
|
||||||
r *http.Request
|
|
||||||
playbookService app.PlaybookService
|
|
||||||
playbookRunService app.PlaybookRunService
|
|
||||||
playbookStore app.PlaybookStore
|
|
||||||
categoryService app.CategoryService
|
|
||||||
api playbooks.ServicesAPI
|
|
||||||
logger logrus.FieldLogger
|
|
||||||
config config.Service
|
|
||||||
permissions *app.PermissionsService
|
|
||||||
licenceChecker app.LicenseChecker
|
|
||||||
favoritesLoader *dataloader.Loader[favoriteInfo, bool]
|
|
||||||
playbooksLoader *dataloader.Loader[playbookInfo, *app.Playbook]
|
|
||||||
}
|
|
||||||
|
|
||||||
// When moving over to the multi-product architecture this should be handled by the server.
|
|
||||||
func (h *GraphQLHandler) graphQL(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Limit bodies to 300KiB.
|
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, 300*1024)
|
|
||||||
|
|
||||||
var params struct {
|
|
||||||
Query string `json:"query"`
|
|
||||||
OperationName string `json:"operationName"`
|
|
||||||
Variables map[string]interface{} `json:"variables"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
|
||||||
c.logger.WithError(err).Error("Unable to decode graphql query")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.config.IsConfiguredForDevelopmentAndTesting() {
|
|
||||||
if params.OperationName == "" {
|
|
||||||
c.logger.Warn("Invalid blank operation name")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dataloaders
|
|
||||||
favoritesLoader := dataloader.NewBatchedLoader(graphQLFavoritesLoader[bool], dataloader.WithBatchCapacity[favoriteInfo, bool](loaderBatchCapacity))
|
|
||||||
playbooksLoader := dataloader.NewBatchedLoader(graphQLPlaybooksLoader[*app.Playbook], dataloader.WithBatchCapacity[playbookInfo, *app.Playbook](loaderBatchCapacity))
|
|
||||||
|
|
||||||
graphQLContext := &GraphQLContext{
|
|
||||||
r: r,
|
|
||||||
playbookService: h.playbookService,
|
|
||||||
playbookRunService: h.playbookRunService,
|
|
||||||
categoryService: h.categoryService,
|
|
||||||
api: h.api,
|
|
||||||
logger: c.logger,
|
|
||||||
config: h.config,
|
|
||||||
permissions: h.permissions,
|
|
||||||
playbookStore: h.playbookStore,
|
|
||||||
licenceChecker: h.licenceChecker,
|
|
||||||
favoritesLoader: favoritesLoader,
|
|
||||||
playbooksLoader: playbooksLoader,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the context with required info.
|
|
||||||
reqCtx := r.Context()
|
|
||||||
reqCtx = context.WithValue(reqCtx, ctxKey{}, graphQLContext)
|
|
||||||
|
|
||||||
response := h.schema.Exec(reqCtx,
|
|
||||||
params.Query,
|
|
||||||
params.OperationName,
|
|
||||||
params.Variables,
|
|
||||||
)
|
|
||||||
r.Header.Set("X-GQL-Operation", params.OperationName)
|
|
||||||
|
|
||||||
for _, err := range response.Errors {
|
|
||||||
errLogger := c.logger.WithError(err).WithField("operation", params.OperationName)
|
|
||||||
|
|
||||||
if errors.Is(err, app.ErrNoPermissions) {
|
|
||||||
errLogger.Warn("Warning executing request")
|
|
||||||
} else if err.Rule == "FieldsOnCorrectType" {
|
|
||||||
errLogger.Warn("Query for non existent field")
|
|
||||||
} else {
|
|
||||||
errLogger.Error("Error executing request")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
|
||||||
c.logger.WithError(err).Warn("Error while writing response")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getContext(ctx context.Context) (*GraphQLContext, error) {
|
|
||||||
c, ok := ctx.Value(ctxKey{}).(*GraphQLContext)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("custom context not found in context")
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GraphiqlPage is the html base code for the graphiQL query runner
|
|
||||||
//
|
|
||||||
//go:embed graphqli.html
|
|
||||||
var GraphiqlPage []byte
|
|
||||||
|
|
||||||
func graphiQL(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
_, _ = w.Write(GraphiqlPage)
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/graph-gophers/dataloader/v7"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
)
|
|
||||||
|
|
||||||
type favoriteInfo struct {
|
|
||||||
TeamID string
|
|
||||||
UserID string
|
|
||||||
ID string
|
|
||||||
Type app.CategoryItemType
|
|
||||||
}
|
|
||||||
|
|
||||||
func graphQLFavoritesLoader[V bool](ctx context.Context, keys []favoriteInfo) []*dataloader.Result[V] {
|
|
||||||
result := make([]*dataloader.Result[V], len(keys))
|
|
||||||
if len(keys) == 0 {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
for i := range keys {
|
|
||||||
result[i] = &dataloader.Result[V]{Error: err}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// assume all keys are for the same team and user
|
|
||||||
teamID := keys[0].TeamID
|
|
||||||
userID := keys[0].UserID
|
|
||||||
|
|
||||||
categoryItems := make([]app.CategoryItem, len(keys))
|
|
||||||
for i, favorite := range keys {
|
|
||||||
categoryItems[i] = app.CategoryItem{
|
|
||||||
ItemID: favorite.ID,
|
|
||||||
Type: favorite.Type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
favorites, err := c.categoryService.AreItemsFavorites(categoryItems, teamID, userID)
|
|
||||||
if err != nil {
|
|
||||||
populateResultWithError(err, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, fav := range favorites {
|
|
||||||
result[i] = &dataloader.Result[V]{Data: V(fav)}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/graph-gophers/dataloader/v7"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
)
|
|
||||||
|
|
||||||
type playbookInfo struct {
|
|
||||||
UserID string
|
|
||||||
TeamID string
|
|
||||||
ID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func graphQLPlaybooksLoader[V *app.Playbook](ctx context.Context, keys []playbookInfo) []*dataloader.Result[V] {
|
|
||||||
result := make([]*dataloader.Result[V], len(keys))
|
|
||||||
|
|
||||||
if len(keys) == 0 {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
uniquePlaybookIDs := getUniquePlaybookIDs(keys)
|
|
||||||
|
|
||||||
var teamID, userID string = keys[0].TeamID, keys[0].UserID
|
|
||||||
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return populateResultWithError(err, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookResult, err := c.playbookService.GetPlaybooksForTeam(
|
|
||||||
app.RequesterInfo{
|
|
||||||
UserID: userID,
|
|
||||||
TeamID: teamID,
|
|
||||||
},
|
|
||||||
teamID,
|
|
||||||
app.PlaybookFilterOptions{
|
|
||||||
PlaybookIDs: uniquePlaybookIDs,
|
|
||||||
PerPage: loaderBatchCapacity,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return populateResultWithError(err, result)
|
|
||||||
}
|
|
||||||
playbooksByID := make(map[string]*app.Playbook)
|
|
||||||
for i := range playbookResult.Items {
|
|
||||||
playbooksByID[playbookResult.Items[i].ID] = &playbookResult.Items[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, playbookInfo := range keys {
|
|
||||||
playbook, ok := playbooksByID[playbookInfo.ID]
|
|
||||||
if !ok {
|
|
||||||
result[i] = &dataloader.Result[V]{Data: nil}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result[i] = &dataloader.Result[V]{
|
|
||||||
Data: V(playbook),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUniquePlaybookIDs(playbooks []playbookInfo) []string {
|
|
||||||
playbookByID := make(map[string]bool)
|
|
||||||
|
|
||||||
for _, playbook := range playbooks {
|
|
||||||
playbookByID[playbook.ID] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]string, 0, len(playbookByID))
|
|
||||||
for playbookID := range playbookByID {
|
|
||||||
result = append(result, playbookID)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PlaybookResolver struct {
|
|
||||||
app.Playbook
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookResolver) ChannelMode(ctx context.Context) string {
|
|
||||||
return fmt.Sprint(r.Playbook.ChannelMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookResolver) IsFavorite(ctx context.Context) (bool, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
thunk := c.favoritesLoader.Load(ctx, favoriteInfo{
|
|
||||||
TeamID: r.TeamID,
|
|
||||||
UserID: userID,
|
|
||||||
Type: app.PlaybookItemType,
|
|
||||||
ID: r.ID,
|
|
||||||
})
|
|
||||||
|
|
||||||
result, err := thunk()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookResolver) DeleteAt() float64 {
|
|
||||||
return float64(r.Playbook.DeleteAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookResolver) LastRunAt() float64 {
|
|
||||||
return float64(r.Playbook.LastRunAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookResolver) NumRuns() int32 {
|
|
||||||
return int32(r.Playbook.NumRuns)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookResolver) ActiveRuns() int32 {
|
|
||||||
return int32(r.Playbook.ActiveRuns)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookResolver) RetrospectiveReminderIntervalSeconds() float64 {
|
|
||||||
return float64(r.Playbook.RetrospectiveReminderIntervalSeconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookResolver) ReminderTimerDefaultSeconds() float64 {
|
|
||||||
return float64(r.Playbook.ReminderTimerDefaultSeconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookResolver) Metrics() []*MetricConfigResolver {
|
|
||||||
metricConfigResolvers := make([]*MetricConfigResolver, 0, len(r.Playbook.Metrics))
|
|
||||||
for _, metricConfig := range r.Playbook.Metrics {
|
|
||||||
metricConfigResolvers = append(metricConfigResolvers, &MetricConfigResolver{metricConfig})
|
|
||||||
}
|
|
||||||
|
|
||||||
return metricConfigResolvers
|
|
||||||
}
|
|
||||||
|
|
||||||
type MetricConfigResolver struct {
|
|
||||||
app.PlaybookMetricConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MetricConfigResolver) Target() *int32 {
|
|
||||||
if r.PlaybookMetricConfig.Target.Valid {
|
|
||||||
intvalue := int32(r.PlaybookMetricConfig.Target.ValueOrZero())
|
|
||||||
return &intvalue
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookResolver) Checklists() []*ChecklistResolver {
|
|
||||||
checklistResolvers := make([]*ChecklistResolver, 0, len(r.Playbook.Checklists))
|
|
||||||
for _, checklist := range r.Playbook.Checklists {
|
|
||||||
checklistResolvers = append(checklistResolvers, &ChecklistResolver{checklist})
|
|
||||||
}
|
|
||||||
|
|
||||||
return checklistResolvers
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChecklistResolver struct {
|
|
||||||
app.Checklist
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ChecklistResolver) Items() []*ChecklistItemResolver {
|
|
||||||
checklistItemResolvers := make([]*ChecklistItemResolver, 0, len(r.Checklist.Items))
|
|
||||||
for _, items := range r.Checklist.Items {
|
|
||||||
checklistItemResolvers = append(checklistItemResolvers, &ChecklistItemResolver{items})
|
|
||||||
}
|
|
||||||
|
|
||||||
return checklistItemResolvers
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChecklistItemResolver struct {
|
|
||||||
app.ChecklistItem
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ChecklistItemResolver) StateModified() float64 {
|
|
||||||
return float64(r.ChecklistItem.StateModified)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ChecklistItemResolver) AssigneeModified() float64 {
|
|
||||||
return float64(r.ChecklistItem.AssigneeModified)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ChecklistItemResolver) CommandLastRun() float64 {
|
|
||||||
return float64(r.ChecklistItem.CommandLastRun)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ChecklistItemResolver) DueDate() float64 {
|
|
||||||
return float64(r.ChecklistItem.DueDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ChecklistItemResolver) TaskActions() []*TaskActionResolver {
|
|
||||||
taskActionsResolvers := make([]*TaskActionResolver, 0, len(r.ChecklistItem.TaskActions))
|
|
||||||
for _, taskAction := range r.ChecklistItem.TaskActions {
|
|
||||||
taskActionsResolvers = append(taskActionsResolvers, &TaskActionResolver{taskAction})
|
|
||||||
}
|
|
||||||
|
|
||||||
return taskActionsResolvers
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaskActionResolver struct {
|
|
||||||
app.TaskAction
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *TaskActionResolver) Trigger() *TriggerResolver {
|
|
||||||
return &TriggerResolver{r.TaskAction.Trigger}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *TaskActionResolver) Actions() []*ActionResolver {
|
|
||||||
actionsResolvers := make([]*ActionResolver, 0, len(r.TaskAction.Actions))
|
|
||||||
for _, action := range r.TaskAction.Actions {
|
|
||||||
actionsResolvers = append(actionsResolvers, &ActionResolver{action})
|
|
||||||
}
|
|
||||||
return actionsResolvers
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActionResolver struct {
|
|
||||||
app.Action
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ActionResolver) Type() string {
|
|
||||||
return string(r.Action.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ActionResolver) Payload() string {
|
|
||||||
var payload string
|
|
||||||
switch r.Action.Type {
|
|
||||||
case app.MarkItemAsDoneActionType:
|
|
||||||
payload = r.Action.Payload
|
|
||||||
default:
|
|
||||||
logrus.WithField("task_action_type", r.Action.Type).Error("Unknown trigger type")
|
|
||||||
payload = ""
|
|
||||||
}
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
|
|
||||||
type TriggerResolver struct {
|
|
||||||
app.Trigger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *TriggerResolver) Type() string {
|
|
||||||
return string(r.Trigger.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *TriggerResolver) Payload() string {
|
|
||||||
var payload string
|
|
||||||
switch r.Trigger.Type {
|
|
||||||
case app.KeywordsByUsersTriggerType:
|
|
||||||
payload = r.Trigger.Payload
|
|
||||||
default:
|
|
||||||
logrus.WithField("task_trigger_type", r.Trigger.Type).Error("Unknown trigger type")
|
|
||||||
payload = ""
|
|
||||||
}
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateChecklist struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Items []UpdateChecklistItem `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c UpdateChecklist) GetItems() []app.ChecklistItemCommon {
|
|
||||||
items := make([]app.ChecklistItemCommon, len(c.Items))
|
|
||||||
for i := range c.Items {
|
|
||||||
items[i] = &c.Items[i]
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateChecklistItem struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
State string `json:"state"`
|
|
||||||
StateModified float64 `json:"state_modified"`
|
|
||||||
AssigneeID string `json:"assignee_id"`
|
|
||||||
AssigneeModified float64 `json:"assignee_modified"`
|
|
||||||
Command string `json:"command"`
|
|
||||||
CommandLastRun float64 `json:"command_last_run"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
LastSkipped float64 `json:"delete_at"`
|
|
||||||
DueDate float64 `json:"due_date"`
|
|
||||||
TaskActions *[]app.TaskAction `json:"task_actions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *UpdateChecklistItem) GetAssigneeID() string {
|
|
||||||
return ci.AssigneeID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *UpdateChecklistItem) SetAssigneeModified(modified int64) {
|
|
||||||
ci.AssigneeModified = float64(modified)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *UpdateChecklistItem) SetState(state string) {
|
|
||||||
ci.State = state
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *UpdateChecklistItem) SetStateModified(modified int64) {
|
|
||||||
ci.StateModified = float64(modified)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *UpdateChecklistItem) SetCommandLastRun(lastRun int64) {
|
|
||||||
ci.CommandLastRun = float64(lastRun)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RootResolver struct {
|
|
||||||
RunRootResolver
|
|
||||||
PlaybookRootResolver
|
|
||||||
}
|
|
||||||
|
|
||||||
func addToSetmap[T any](setmap map[string]interface{}, name string, value *T) {
|
|
||||||
if value != nil {
|
|
||||||
setmap[name] = *value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addConcatToSetmap(setmap map[string]interface{}, name string, value *[]string) {
|
|
||||||
if value != nil {
|
|
||||||
setmap[name] = strings.Join(*value, ",")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,549 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gopkg.in/guregu/null.v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RunMutationCollection hold all mutation functions for a playbookRun
|
|
||||||
type PlaybookRootResolver struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func getGraphqlPlaybook(ctx context.Context, playbookID string) (*PlaybookResolver, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
if err = c.permissions.PlaybookView(userID, playbookID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
playbook, err := c.playbookService.Get(playbookID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &PlaybookResolver{playbook}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookRootResolver) Playbook(ctx context.Context, args struct {
|
|
||||||
ID string
|
|
||||||
}) (*PlaybookResolver, error) {
|
|
||||||
playbookID := args.ID
|
|
||||||
return getGraphqlPlaybook(ctx, playbookID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookRootResolver) Playbooks(ctx context.Context, args struct {
|
|
||||||
TeamID string
|
|
||||||
Sort string
|
|
||||||
Direction string
|
|
||||||
SearchTerm string
|
|
||||||
WithMembershipOnly bool
|
|
||||||
WithArchived bool
|
|
||||||
}) ([]*PlaybookResolver, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
if args.TeamID != "" {
|
|
||||||
if err = c.permissions.PlaybookList(userID, args.TeamID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isGuest, err := app.IsGuest(userID, c.api)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
requesterInfo := app.RequesterInfo{
|
|
||||||
UserID: userID,
|
|
||||||
TeamID: args.TeamID,
|
|
||||||
IsAdmin: app.IsSystemAdmin(userID, c.api),
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := app.PlaybookFilterOptions{
|
|
||||||
Sort: app.SortField(args.Sort),
|
|
||||||
Direction: app.SortDirection(args.Direction),
|
|
||||||
SearchTerm: args.SearchTerm,
|
|
||||||
WithArchived: args.WithArchived,
|
|
||||||
WithMembershipOnly: isGuest || args.WithMembershipOnly, // Guests can only see playbooks if they are invited to them
|
|
||||||
Page: 0,
|
|
||||||
PerPage: 10000,
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookResults, err := c.playbookService.GetPlaybooksForTeam(requesterInfo, args.TeamID, opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ret := make([]*PlaybookResolver, 0, len(playbookResults.Items))
|
|
||||||
for _, pb := range playbookResults.Items {
|
|
||||||
ret = append(ret, &PlaybookResolver{pb})
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunRootResolver) UpdatePlaybookFavorite(ctx context.Context, args struct {
|
|
||||||
ID string
|
|
||||||
Favorite bool
|
|
||||||
}) (string, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
if err = c.permissions.PlaybookView(userID, args.ID); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPlaybook, err := c.playbookService.Get(args.ID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentPlaybook.DeleteAt != 0 {
|
|
||||||
return "", errors.New("archived playbooks can not be modified")
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.Favorite {
|
|
||||||
if err := c.categoryService.AddFavorite(
|
|
||||||
app.CategoryItem{
|
|
||||||
ItemID: currentPlaybook.ID,
|
|
||||||
Type: app.PlaybookItemType,
|
|
||||||
},
|
|
||||||
currentPlaybook.TeamID,
|
|
||||||
userID,
|
|
||||||
); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := c.categoryService.DeleteFavorite(
|
|
||||||
app.CategoryItem{
|
|
||||||
ItemID: currentPlaybook.ID,
|
|
||||||
Type: app.PlaybookItemType,
|
|
||||||
},
|
|
||||||
currentPlaybook.TeamID,
|
|
||||||
userID,
|
|
||||||
); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentPlaybook.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookRootResolver) UpdatePlaybook(ctx context.Context, args struct {
|
|
||||||
ID string
|
|
||||||
Updates struct {
|
|
||||||
Title *string
|
|
||||||
Description *string
|
|
||||||
Public *bool
|
|
||||||
CreatePublicPlaybookRun *bool
|
|
||||||
ReminderMessageTemplate *string
|
|
||||||
ReminderTimerDefaultSeconds *float64
|
|
||||||
StatusUpdateEnabled *bool
|
|
||||||
InvitedUserIDs *[]string
|
|
||||||
InvitedGroupIDs *[]string
|
|
||||||
InviteUsersEnabled *bool
|
|
||||||
DefaultOwnerID *string
|
|
||||||
DefaultOwnerEnabled *bool
|
|
||||||
BroadcastChannelIDs *[]string
|
|
||||||
BroadcastEnabled *bool
|
|
||||||
WebhookOnCreationURLs *[]string
|
|
||||||
WebhookOnCreationEnabled *bool
|
|
||||||
MessageOnJoin *string
|
|
||||||
MessageOnJoinEnabled *bool
|
|
||||||
RetrospectiveReminderIntervalSeconds *float64
|
|
||||||
RetrospectiveTemplate *string
|
|
||||||
RetrospectiveEnabled *bool
|
|
||||||
WebhookOnStatusUpdateURLs *[]string
|
|
||||||
WebhookOnStatusUpdateEnabled *bool
|
|
||||||
SignalAnyKeywords *[]string
|
|
||||||
SignalAnyKeywordsEnabled *bool
|
|
||||||
CategorizeChannelEnabled *bool
|
|
||||||
CategoryName *string
|
|
||||||
RunSummaryTemplateEnabled *bool
|
|
||||||
RunSummaryTemplate *string
|
|
||||||
ChannelNameTemplate *string
|
|
||||||
Checklists *[]UpdateChecklist
|
|
||||||
CreateChannelMemberOnNewParticipant *bool
|
|
||||||
RemoveChannelMemberOnRemovedParticipant *bool
|
|
||||||
ChannelID *string
|
|
||||||
ChannelMode *string
|
|
||||||
}
|
|
||||||
}) (string, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
currentPlaybook, err := c.playbookService.Get(args.ID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.permissions.PlaybookManageProperties(userID, currentPlaybook); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentPlaybook.DeleteAt != 0 {
|
|
||||||
return "", errors.New("archived playbooks can not be modified")
|
|
||||||
}
|
|
||||||
|
|
||||||
setmap := map[string]interface{}{}
|
|
||||||
addToSetmap(setmap, "Title", args.Updates.Title)
|
|
||||||
addToSetmap(setmap, "Description", args.Updates.Description)
|
|
||||||
if args.Updates.Public != nil {
|
|
||||||
if *args.Updates.Public {
|
|
||||||
if err := c.permissions.PlaybookMakePublic(userID, currentPlaybook); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := c.permissions.PlaybookMakePrivate(userID, currentPlaybook); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !c.licenceChecker.PlaybookAllowed(*args.Updates.Public) {
|
|
||||||
return "", errors.Wrapf(app.ErrLicensedFeature, "the playbook is not valid with the current license")
|
|
||||||
}
|
|
||||||
addToSetmap(setmap, "Public", args.Updates.Public)
|
|
||||||
}
|
|
||||||
addToSetmap(setmap, "CreatePublicIncident", args.Updates.CreatePublicPlaybookRun)
|
|
||||||
addToSetmap(setmap, "ReminderMessageTemplate", args.Updates.ReminderMessageTemplate)
|
|
||||||
addToSetmap(setmap, "ReminderTimerDefaultSeconds", args.Updates.ReminderTimerDefaultSeconds)
|
|
||||||
addToSetmap(setmap, "StatusUpdateEnabled", args.Updates.StatusUpdateEnabled)
|
|
||||||
addToSetmap(setmap, "CreateChannelMemberOnNewParticipant", args.Updates.CreateChannelMemberOnNewParticipant)
|
|
||||||
addToSetmap(setmap, "RemoveChannelMemberOnRemovedParticipant", args.Updates.RemoveChannelMemberOnRemovedParticipant)
|
|
||||||
|
|
||||||
if args.Updates.InvitedUserIDs != nil {
|
|
||||||
filteredInvitedUserIDs := c.permissions.FilterInvitedUserIDs(*args.Updates.InvitedUserIDs, currentPlaybook.TeamID)
|
|
||||||
addConcatToSetmap(setmap, "ConcatenatedInvitedUserIDs", &filteredInvitedUserIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.Updates.InvitedGroupIDs != nil {
|
|
||||||
filteredInvitedGroupIDs := c.permissions.FilterInvitedGroupIDs(*args.Updates.InvitedGroupIDs)
|
|
||||||
addConcatToSetmap(setmap, "ConcatenatedInvitedGroupIDs", &filteredInvitedGroupIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
addToSetmap(setmap, "InviteUsersEnabled", args.Updates.InviteUsersEnabled)
|
|
||||||
if args.Updates.DefaultOwnerID != nil {
|
|
||||||
if !c.api.HasPermissionToTeam(*args.Updates.DefaultOwnerID, currentPlaybook.TeamID, model.PermissionViewTeam) {
|
|
||||||
return "", errors.Wrap(app.ErrNoPermissions, "default owner can't view team")
|
|
||||||
}
|
|
||||||
addToSetmap(setmap, "DefaultCommanderID", args.Updates.DefaultOwnerID)
|
|
||||||
}
|
|
||||||
addToSetmap(setmap, "DefaultCommanderEnabled", args.Updates.DefaultOwnerEnabled)
|
|
||||||
|
|
||||||
if args.Updates.BroadcastChannelIDs != nil {
|
|
||||||
if err := c.permissions.NoAddedBroadcastChannelsWithoutPermission(userID, *args.Updates.BroadcastChannelIDs, currentPlaybook.BroadcastChannelIDs); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
addConcatToSetmap(setmap, "ConcatenatedBroadcastChannelIDs", args.Updates.BroadcastChannelIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
addToSetmap(setmap, "BroadcastEnabled", args.Updates.BroadcastEnabled)
|
|
||||||
if args.Updates.WebhookOnCreationURLs != nil {
|
|
||||||
if err := app.ValidateWebhookURLs(*args.Updates.WebhookOnCreationURLs); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
addConcatToSetmap(setmap, "ConcatenatedWebhookOnCreationURLs", args.Updates.WebhookOnCreationURLs)
|
|
||||||
}
|
|
||||||
addToSetmap(setmap, "WebhookOnCreationEnabled", args.Updates.WebhookOnCreationEnabled)
|
|
||||||
addToSetmap(setmap, "MessageOnJoin", args.Updates.MessageOnJoin)
|
|
||||||
addToSetmap(setmap, "MessageOnJoinEnabled", args.Updates.MessageOnJoinEnabled)
|
|
||||||
addToSetmap(setmap, "RetrospectiveReminderIntervalSeconds", args.Updates.RetrospectiveReminderIntervalSeconds)
|
|
||||||
addToSetmap(setmap, "RetrospectiveTemplate", args.Updates.RetrospectiveTemplate)
|
|
||||||
addToSetmap(setmap, "RetrospectiveEnabled", args.Updates.RetrospectiveEnabled)
|
|
||||||
if args.Updates.WebhookOnStatusUpdateURLs != nil {
|
|
||||||
if err := app.ValidateWebhookURLs(*args.Updates.WebhookOnStatusUpdateURLs); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
addConcatToSetmap(setmap, "ConcatenatedWebhookOnStatusUpdateURLs", args.Updates.WebhookOnStatusUpdateURLs)
|
|
||||||
}
|
|
||||||
addToSetmap(setmap, "WebhookOnStatusUpdateEnabled", args.Updates.WebhookOnStatusUpdateEnabled)
|
|
||||||
if args.Updates.SignalAnyKeywords != nil {
|
|
||||||
validSignalAnyKeywords := app.ProcessSignalAnyKeywords(*args.Updates.SignalAnyKeywords)
|
|
||||||
addConcatToSetmap(setmap, "ConcatenatedSignalAnyKeywords", &validSignalAnyKeywords)
|
|
||||||
}
|
|
||||||
addToSetmap(setmap, "SignalAnyKeywordsEnabled", args.Updates.SignalAnyKeywordsEnabled)
|
|
||||||
addToSetmap(setmap, "CategorizeChannelEnabled", args.Updates.CategorizeChannelEnabled)
|
|
||||||
if args.Updates.CategoryName != nil {
|
|
||||||
if err := app.ValidateCategoryName(*args.Updates.CategoryName); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
addToSetmap(setmap, "CategoryName", args.Updates.CategoryName)
|
|
||||||
}
|
|
||||||
addToSetmap(setmap, "RunSummaryTemplateEnabled", args.Updates.RunSummaryTemplateEnabled)
|
|
||||||
addToSetmap(setmap, "RunSummaryTemplate", args.Updates.RunSummaryTemplate)
|
|
||||||
addToSetmap(setmap, "ChannelNameTemplate", args.Updates.ChannelNameTemplate)
|
|
||||||
addToSetmap(setmap, "ChannelID", args.Updates.ChannelID)
|
|
||||||
addToSetmap(setmap, "ChannelMode", args.Updates.ChannelMode)
|
|
||||||
|
|
||||||
// Not optimal graphql. Stopgap measure. Should be updated separately.
|
|
||||||
if args.Updates.Checklists != nil {
|
|
||||||
app.CleanUpChecklists(*args.Updates.Checklists)
|
|
||||||
if err := validateUpdateTaskActions(*args.Updates.Checklists); err != nil {
|
|
||||||
return "", errors.Wrapf(err, "failed to validate task actions in graphql json for playbook id: '%s'", args.ID)
|
|
||||||
}
|
|
||||||
checklistsJSON, err := json.Marshal(args.Updates.Checklists)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrapf(err, "failed to marshal checklist in graphql json for playbook id: '%s'", args.ID)
|
|
||||||
}
|
|
||||||
setmap["ChecklistsJSON"] = checklistsJSON
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.Updates.Checklists != nil || args.Updates.InvitedUserIDs != nil || args.Updates.InviteUsersEnabled != nil {
|
|
||||||
if err := validatePreAssignmentUpdate(currentPlaybook, args.Updates.Checklists, args.Updates.InvitedUserIDs, args.Updates.InviteUsersEnabled); err != nil {
|
|
||||||
return "", errors.Wrapf(err, "invalid user pre-assignment for playbook id: '%s'", args.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(setmap) > 0 {
|
|
||||||
if err := c.playbookStore.GraphqlUpdate(args.ID, setmap); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return args.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookRootResolver) AddPlaybookMember(ctx context.Context, args struct {
|
|
||||||
PlaybookID string
|
|
||||||
UserID string
|
|
||||||
}) (string, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
currentPlaybook, err := c.playbookService.Get(args.PlaybookID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.permissions.PlaybookManageMembers(userID, currentPlaybook); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentPlaybook.DeleteAt != 0 {
|
|
||||||
return "", errors.New("archived playbooks can not be modified")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.playbookStore.AddPlaybookMember(args.PlaybookID, args.UserID); err != nil {
|
|
||||||
return "", errors.Wrap(err, "unable to add playbook member")
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookRootResolver) RemovePlaybookMember(ctx context.Context, args struct {
|
|
||||||
PlaybookID string
|
|
||||||
UserID string
|
|
||||||
}) (string, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
currentPlaybook, err := c.playbookService.Get(args.PlaybookID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentPlaybook.DeleteAt != 0 {
|
|
||||||
return "", errors.New("archived playbooks can not be modified")
|
|
||||||
}
|
|
||||||
|
|
||||||
// do not require manageMembers permission if the user want to leave playbook
|
|
||||||
if userID != args.UserID {
|
|
||||||
if err := c.permissions.PlaybookManageMembers(userID, currentPlaybook); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.playbookStore.RemovePlaybookMember(args.PlaybookID, args.UserID); err != nil {
|
|
||||||
return "", errors.Wrap(err, "unable to remove playbook member")
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookRootResolver) AddMetric(ctx context.Context, args struct {
|
|
||||||
PlaybookID string
|
|
||||||
Title string
|
|
||||||
Description string
|
|
||||||
Type string
|
|
||||||
Target *float64
|
|
||||||
}) (string, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
currentPlaybook, err := c.playbookService.Get(args.PlaybookID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentPlaybook.DeleteAt != 0 {
|
|
||||||
return "", errors.New("archived playbooks can not be modified")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.permissions.PlaybookManageProperties(userID, currentPlaybook); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var target null.Int
|
|
||||||
if args.Target == nil {
|
|
||||||
target = null.NewInt(0, false)
|
|
||||||
} else {
|
|
||||||
target = null.IntFrom(int64(*args.Target))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.playbookStore.AddMetric(args.PlaybookID, app.PlaybookMetricConfig{
|
|
||||||
Title: args.Title,
|
|
||||||
Description: args.Description,
|
|
||||||
Type: args.Type,
|
|
||||||
Target: target,
|
|
||||||
}); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return args.PlaybookID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookRootResolver) UpdateMetric(ctx context.Context, args struct {
|
|
||||||
ID string
|
|
||||||
Title *string
|
|
||||||
Description *string
|
|
||||||
Target *float64
|
|
||||||
}) (string, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
currentMetric, err := c.playbookStore.GetMetric(args.ID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPlaybook, err := c.playbookService.Get(currentMetric.PlaybookID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentPlaybook.DeleteAt != 0 {
|
|
||||||
return "", errors.New("archived playbooks can not be modified")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.permissions.PlaybookManageProperties(userID, currentPlaybook); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
setmap := map[string]interface{}{}
|
|
||||||
addToSetmap(setmap, "Title", args.Title)
|
|
||||||
addToSetmap(setmap, "Description", args.Description)
|
|
||||||
if args.Target != nil {
|
|
||||||
setmap["Target"] = null.IntFrom(int64(*args.Target))
|
|
||||||
}
|
|
||||||
if len(setmap) > 0 {
|
|
||||||
if err := c.playbookStore.UpdateMetric(args.ID, setmap); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return args.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlaybookRootResolver) DeleteMetric(ctx context.Context, args struct {
|
|
||||||
ID string
|
|
||||||
}) (string, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
currentMetric, err := c.playbookStore.GetMetric(args.ID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPlaybook, err := c.playbookService.Get(currentMetric.PlaybookID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.permissions.PlaybookManageProperties(userID, currentPlaybook); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.playbookStore.DeleteMetric(args.ID); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return args.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validatePreAssignmentUpdate[T app.ChecklistCommon](pb app.Playbook, newChecklists *[]T, newInvitedUsers *[]string, newInviteUsersEnabled *bool) error {
|
|
||||||
assignees := app.GetDistinctAssignees(pb.Checklists)
|
|
||||||
if newChecklists != nil {
|
|
||||||
assignees = app.GetDistinctAssignees(*newChecklists)
|
|
||||||
}
|
|
||||||
|
|
||||||
invitedUsers := pb.InvitedUserIDs
|
|
||||||
if newInvitedUsers != nil {
|
|
||||||
invitedUsers = *newInvitedUsers
|
|
||||||
}
|
|
||||||
|
|
||||||
inviteUsersEnabled := pb.InviteUsersEnabled
|
|
||||||
if newInviteUsersEnabled != nil {
|
|
||||||
inviteUsersEnabled = *newInviteUsersEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.ValidatePreAssignment(assignees, invitedUsers, inviteUsersEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateUpdateTaskActions validates the taskactions in the given checklist
|
|
||||||
// NOTE: Any changes to this function must be made to function 'validateTaskActions' for the REST endpoint.
|
|
||||||
func validateUpdateTaskActions(checklists []UpdateChecklist) error {
|
|
||||||
for _, checklist := range checklists {
|
|
||||||
for _, item := range checklist.Items {
|
|
||||||
if taskActions := item.TaskActions; taskActions != nil {
|
|
||||||
for _, ta := range *taskActions {
|
|
||||||
if err := app.ValidateTrigger(ta.Trigger); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, a := range ta.Actions {
|
|
||||||
if err := app.ValidateAction(a); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/client"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RunRootResolver hold all queries and mutations for a playbookRun
|
|
||||||
type RunRootResolver struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunRootResolver) Run(ctx context.Context, args struct {
|
|
||||||
ID string `url:"id,omitempty"`
|
|
||||||
}) (*RunResolver, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
if err = c.permissions.RunView(userID, args.ID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
run, err := c.playbookRunService.GetPlaybookRun(args.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &RunResolver{*run}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunRootResolver) Runs(ctx context.Context, args struct {
|
|
||||||
TeamID string
|
|
||||||
Sort string
|
|
||||||
Direction string
|
|
||||||
Statuses []string
|
|
||||||
ParticipantOrFollowerID string
|
|
||||||
ChannelID string
|
|
||||||
First *int32
|
|
||||||
After *string
|
|
||||||
Types []string
|
|
||||||
}) (*RunConnectionResolver, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
requesterInfo := app.RequesterInfo{
|
|
||||||
UserID: userID,
|
|
||||||
TeamID: args.TeamID,
|
|
||||||
IsAdmin: app.IsSystemAdmin(userID, c.api),
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.ParticipantOrFollowerID == client.Me {
|
|
||||||
args.ParticipantOrFollowerID = userID
|
|
||||||
}
|
|
||||||
|
|
||||||
perPage := 10000 // If paging not specified, get "everything"
|
|
||||||
if args.First != nil {
|
|
||||||
perPage = int(*args.First)
|
|
||||||
}
|
|
||||||
|
|
||||||
page := 0
|
|
||||||
if args.After != nil {
|
|
||||||
page, err = decodeRunConnectionCursor(*args.After)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filterOptions := app.PlaybookRunFilterOptions{
|
|
||||||
Sort: app.SortField(args.Sort),
|
|
||||||
Direction: app.SortDirection(args.Direction),
|
|
||||||
TeamID: args.TeamID,
|
|
||||||
Statuses: args.Statuses,
|
|
||||||
ParticipantOrFollowerID: args.ParticipantOrFollowerID,
|
|
||||||
ChannelID: args.ChannelID,
|
|
||||||
IncludeFavorites: true,
|
|
||||||
Types: args.Types,
|
|
||||||
Page: page,
|
|
||||||
PerPage: perPage,
|
|
||||||
}
|
|
||||||
|
|
||||||
runResults, err := c.playbookRunService.GetPlaybookRuns(requesterInfo, filterOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &RunConnectionResolver{results: *runResults, page: page}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunRootResolver) SetRunFavorite(ctx context.Context, args struct {
|
|
||||||
ID string
|
|
||||||
Fav bool
|
|
||||||
}) (string, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
if err = c.permissions.RunView(userID, args.ID); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookRun, err := c.playbookRunService.GetPlaybookRun(args.ID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.Fav {
|
|
||||||
if err := c.categoryService.AddFavorite(
|
|
||||||
app.CategoryItem{
|
|
||||||
ItemID: playbookRun.ID,
|
|
||||||
Type: app.RunItemType,
|
|
||||||
},
|
|
||||||
playbookRun.TeamID,
|
|
||||||
userID,
|
|
||||||
); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := c.categoryService.DeleteFavorite(
|
|
||||||
app.CategoryItem{
|
|
||||||
ItemID: playbookRun.ID,
|
|
||||||
Type: app.RunItemType,
|
|
||||||
},
|
|
||||||
playbookRun.TeamID,
|
|
||||||
userID,
|
|
||||||
); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return playbookRun.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type RunUpdates struct {
|
|
||||||
Name *string
|
|
||||||
Summary *string
|
|
||||||
ChannelID *string
|
|
||||||
CreateChannelMemberOnNewParticipant *bool
|
|
||||||
RemoveChannelMemberOnRemovedParticipant *bool
|
|
||||||
StatusUpdateBroadcastChannelsEnabled *bool
|
|
||||||
StatusUpdateBroadcastWebhooksEnabled *bool
|
|
||||||
BroadcastChannelIDs *[]string
|
|
||||||
WebhookOnStatusUpdateURLs *[]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunRootResolver) UpdateRun(ctx context.Context, args struct {
|
|
||||||
ID string
|
|
||||||
Updates RunUpdates
|
|
||||||
}) (string, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
if err = c.permissions.RunManageProperties(userID, args.ID); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookRun, err := c.playbookRunService.GetPlaybookRun(args.ID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
now := model.GetMillis()
|
|
||||||
|
|
||||||
// scalar updates
|
|
||||||
setmap := map[string]interface{}{}
|
|
||||||
addToSetmap(setmap, "Name", args.Updates.Name)
|
|
||||||
addToSetmap(setmap, "Description", args.Updates.Summary)
|
|
||||||
addToSetmap(setmap, "ChannelID", args.Updates.ChannelID)
|
|
||||||
addToSetmap(setmap, "CreateChannelMemberOnNewParticipant", args.Updates.CreateChannelMemberOnNewParticipant)
|
|
||||||
addToSetmap(setmap, "RemoveChannelMemberOnRemovedParticipant", args.Updates.RemoveChannelMemberOnRemovedParticipant)
|
|
||||||
addToSetmap(setmap, "StatusUpdateBroadcastChannelsEnabled", args.Updates.StatusUpdateBroadcastChannelsEnabled)
|
|
||||||
addToSetmap(setmap, "StatusUpdateBroadcastWebhooksEnabled", args.Updates.StatusUpdateBroadcastWebhooksEnabled)
|
|
||||||
|
|
||||||
if args.Updates.Summary != nil {
|
|
||||||
addToSetmap(setmap, "SummaryModifiedAt", &now)
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.Updates.BroadcastChannelIDs != nil {
|
|
||||||
if err := c.permissions.NoAddedBroadcastChannelsWithoutPermission(userID, *args.Updates.BroadcastChannelIDs, playbookRun.BroadcastChannelIDs); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
addConcatToSetmap(setmap, "ConcatenatedBroadcastChannelIDs", args.Updates.BroadcastChannelIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.Updates.WebhookOnStatusUpdateURLs != nil {
|
|
||||||
if err := app.ValidateWebhookURLs(*args.Updates.WebhookOnStatusUpdateURLs); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
addConcatToSetmap(setmap, "ConcatenatedWebhookOnStatusUpdateURLs", args.Updates.WebhookOnStatusUpdateURLs)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.playbookRunService.GraphqlUpdate(args.ID, setmap); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return playbookRun.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunRootResolver) AddRunParticipants(ctx context.Context, args struct {
|
|
||||||
RunID string
|
|
||||||
UserIDs []string
|
|
||||||
ForceAddToChannel bool
|
|
||||||
}) (string, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
// When user is joining run RunView permission is enough, otherwise user need manage permissions
|
|
||||||
if updatesOnlyRequesterMembership(userID, args.UserIDs) {
|
|
||||||
if err := c.permissions.RunView(userID, args.RunID); err != nil {
|
|
||||||
return "", errors.Wrap(err, "attempted to join run without permissions")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := c.permissions.RunManageProperties(userID, args.RunID); err != nil {
|
|
||||||
return "", errors.Wrap(err, "attempted to modify participants without permissions")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.playbookRunService.AddParticipants(args.RunID, args.UserIDs, userID, args.ForceAddToChannel); err != nil {
|
|
||||||
return "", errors.Wrap(err, "failed to add participant from run")
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunRootResolver) RemoveRunParticipants(ctx context.Context, args struct {
|
|
||||||
RunID string
|
|
||||||
UserIDs []string
|
|
||||||
}) (string, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
// When user is leaving run RunView permission is enough, otherwise user need manage permissions
|
|
||||||
if updatesOnlyRequesterMembership(userID, args.UserIDs) {
|
|
||||||
if err := c.permissions.RunView(userID, args.RunID); err != nil {
|
|
||||||
return "", errors.Wrap(err, "attempted to modify participants without permissions")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := c.permissions.RunManageProperties(userID, args.RunID); err != nil {
|
|
||||||
return "", errors.Wrap(err, "attempted to modify participants without permissions")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.playbookRunService.RemoveParticipants(args.RunID, args.UserIDs, userID); err != nil {
|
|
||||||
return "", errors.Wrap(err, "failed to remove participant from run")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, userID := range args.UserIDs {
|
|
||||||
if err := c.playbookRunService.Unfollow(args.RunID, userID); err != nil {
|
|
||||||
return "", errors.Wrap(err, "failed to make participant to unfollow run")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func updatesOnlyRequesterMembership(requesterUserID string, userIDs []string) bool {
|
|
||||||
return len(userIDs) == 1 && userIDs[0] == requesterUserID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunRootResolver) ChangeRunOwner(ctx context.Context, args struct {
|
|
||||||
RunID string
|
|
||||||
OwnerID string
|
|
||||||
}) (string, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
requesterID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
if err := c.permissions.RunManageProperties(requesterID, args.RunID); err != nil {
|
|
||||||
return "", errors.Wrap(err, "attempted to modify the run owner without permissions")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.playbookRunService.ChangeOwner(args.RunID, requesterID, args.OwnerID); err != nil {
|
|
||||||
return "", errors.Wrap(err, "failed to change the run owner")
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunRootResolver) UpdateRunTaskActions(ctx context.Context, args struct {
|
|
||||||
RunID string
|
|
||||||
ChecklistNum float64
|
|
||||||
ItemNum float64
|
|
||||||
TaskActions *[]app.TaskAction
|
|
||||||
}) (string, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if args.TaskActions == nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
if err := validateTaskActions(*args.TaskActions); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.playbookRunService.SetTaskActionsToChecklistItem(args.RunID, userID, int(args.ChecklistNum), int(args.ItemNum), *args.TaskActions); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RunResolver struct {
|
|
||||||
app.PlaybookRun
|
|
||||||
}
|
|
||||||
|
|
||||||
// NumTasks is a computed attribute (not stored in database) which
|
|
||||||
// returns the number of total tasks in a playbook run:
|
|
||||||
func (r *RunResolver) NumTasks() int32 {
|
|
||||||
total := 0
|
|
||||||
for _, checklist := range r.PlaybookRun.Checklists {
|
|
||||||
total += len(checklist.Items)
|
|
||||||
}
|
|
||||||
return int32(total)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NumTasksClosed is a computed attribute (not stored in database) which
|
|
||||||
// returns the number of tasks closed in a playbook run:
|
|
||||||
func (r *RunResolver) NumTasksClosed() int32 {
|
|
||||||
closed := 0
|
|
||||||
for _, checklist := range r.PlaybookRun.Checklists {
|
|
||||||
for _, item := range checklist.Items {
|
|
||||||
if item.State == app.ChecklistItemStateClosed || item.State == app.ChecklistItemStateSkipped {
|
|
||||||
closed++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return int32(closed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) Type() string {
|
|
||||||
return r.PlaybookRun.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) CreateAt() float64 {
|
|
||||||
return float64(r.PlaybookRun.CreateAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) EndAt() float64 {
|
|
||||||
return float64(r.PlaybookRun.EndAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) SummaryModifiedAt() float64 {
|
|
||||||
return float64(r.PlaybookRun.SummaryModifiedAt)
|
|
||||||
}
|
|
||||||
func (r *RunResolver) LastStatusUpdateAt() float64 {
|
|
||||||
return float64(r.PlaybookRun.LastStatusUpdateAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) RetrospectivePublishedAt() float64 {
|
|
||||||
return float64(r.PlaybookRun.RetrospectivePublishedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) ReminderTimerDefaultSeconds() float64 {
|
|
||||||
return float64(r.PlaybookRun.ReminderTimerDefaultSeconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) PreviousReminder() float64 {
|
|
||||||
return float64(r.PlaybookRun.PreviousReminder)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) RetrospectiveReminderIntervalSeconds() float64 {
|
|
||||||
return float64(r.PlaybookRun.RetrospectiveReminderIntervalSeconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) Checklists() []*ChecklistResolver {
|
|
||||||
checklistResolvers := make([]*ChecklistResolver, 0, len(r.PlaybookRun.Checklists))
|
|
||||||
for _, checklist := range r.PlaybookRun.Checklists {
|
|
||||||
checklistResolvers = append(checklistResolvers, &ChecklistResolver{checklist})
|
|
||||||
}
|
|
||||||
|
|
||||||
return checklistResolvers
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) StatusPosts() []*StatusPostResolver {
|
|
||||||
statusPostResolvers := make([]*StatusPostResolver, 0, len(r.PlaybookRun.StatusPosts))
|
|
||||||
for _, statusPost := range r.PlaybookRun.StatusPosts {
|
|
||||||
statusPostResolvers = append(statusPostResolvers, &StatusPostResolver{statusPost})
|
|
||||||
}
|
|
||||||
|
|
||||||
return statusPostResolvers
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) TimelineEvents() []*TimelineEventResolver {
|
|
||||||
timelineEventResolvers := make([]*TimelineEventResolver, 0, len(r.PlaybookRun.StatusPosts))
|
|
||||||
for _, event := range r.PlaybookRun.TimelineEvents {
|
|
||||||
timelineEventResolvers = append(timelineEventResolvers, &TimelineEventResolver{event})
|
|
||||||
}
|
|
||||||
|
|
||||||
return timelineEventResolvers
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) IsFavorite(ctx context.Context) (bool, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
thunk := c.favoritesLoader.Load(ctx, favoriteInfo{
|
|
||||||
TeamID: r.TeamID,
|
|
||||||
UserID: userID,
|
|
||||||
Type: app.RunItemType,
|
|
||||||
ID: r.ID,
|
|
||||||
})
|
|
||||||
|
|
||||||
result, err := thunk()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatusPostResolver struct {
|
|
||||||
app.StatusPost
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StatusPostResolver) CreateAt() float64 {
|
|
||||||
return float64(r.StatusPost.CreateAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StatusPostResolver) DeleteAt() float64 {
|
|
||||||
return float64(r.StatusPost.DeleteAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TimelineEventResolver struct {
|
|
||||||
app.TimelineEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *TimelineEventResolver) CreateAt() float64 {
|
|
||||||
return float64(r.TimelineEvent.CreateAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *TimelineEventResolver) EventType() string {
|
|
||||||
return string(r.TimelineEvent.EventType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *TimelineEventResolver) DeleteAt() float64 {
|
|
||||||
return float64(r.TimelineEvent.DeleteAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) Followers(ctx context.Context) ([]string, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata, err := c.playbookRunService.GetPlaybookRunMetadata(r.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "can't get metadata")
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata.Followers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) Playbook(ctx context.Context) (*PlaybookResolver, error) {
|
|
||||||
c, err := getContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
userID := c.r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
thunk := c.playbooksLoader.Load(ctx, playbookInfo{
|
|
||||||
UserID: userID,
|
|
||||||
ID: r.PlaybookID,
|
|
||||||
TeamID: r.TeamID,
|
|
||||||
})
|
|
||||||
|
|
||||||
result, err := thunk()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if result == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &PlaybookResolver{*result}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunResolver) LastUpdatedAt(ctx context.Context) float64 {
|
|
||||||
if len(r.PlaybookRun.TimelineEvents) < 1 {
|
|
||||||
return float64(r.PlaybookRun.CreateAt)
|
|
||||||
}
|
|
||||||
return float64(r.PlaybookRun.TimelineEvents[len(r.PlaybookRun.TimelineEvents)-1].EventAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
type RunConnectionResolver struct {
|
|
||||||
results app.GetPlaybookRunsResults
|
|
||||||
page int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunConnectionResolver) TotalCount() int32 {
|
|
||||||
return int32(r.results.TotalCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunConnectionResolver) Edges() []*RunEdgeResolver {
|
|
||||||
ret := make([]*RunEdgeResolver, 0, len(r.results.Items))
|
|
||||||
// Cursor is just the end cursor for the page for now
|
|
||||||
cursor := r.results.PageCount
|
|
||||||
for _, run := range r.results.Items {
|
|
||||||
ret = append(ret, &RunEdgeResolver{run, cursor})
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunConnectionResolver) PageInfo() *PageInfoResolver {
|
|
||||||
startCursor := ""
|
|
||||||
endCursor := ""
|
|
||||||
|
|
||||||
if len(r.results.Items) > 0 {
|
|
||||||
// "Cursors" are just the page numbers
|
|
||||||
startCursor = encodeRunConnectionCursor(r.page)
|
|
||||||
endCursor = encodeRunConnectionCursor(r.page + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &PageInfoResolver{
|
|
||||||
HasNextPage: r.results.HasMore,
|
|
||||||
StartCursor: startCursor,
|
|
||||||
EndCursor: endCursor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeRunConnectionCursor(cursor int) string {
|
|
||||||
return strconv.Itoa(cursor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeRunConnectionCursor(cursor string) (int, error) {
|
|
||||||
num, err := strconv.Atoi(cursor)
|
|
||||||
if err != nil {
|
|
||||||
return 0, errors.Wrap(err, "unable to decode cursor")
|
|
||||||
}
|
|
||||||
return num, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type RunEdgeResolver struct {
|
|
||||||
run app.PlaybookRun
|
|
||||||
cursor int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunEdgeResolver) Node() *RunResolver {
|
|
||||||
return &RunResolver{r.run}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunEdgeResolver) Cursor() string {
|
|
||||||
return encodeRunConnectionCursor(r.cursor)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PageInfoResolver struct {
|
|
||||||
HasNextPage bool
|
|
||||||
StartCursor string
|
|
||||||
EndCursor string
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>GraphiQL editor | Mattermost</title>
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.css" integrity="sha256-gSgd+on4bTXigueyd/NSRNAy4cBY42RAVNaXnQDjOW8=" crossorigin="anonymous"/>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js" integrity="sha256-OI3N9zCKabDov2rZFzl8lJUXCcP7EmsGcGoP6DMXQCo=" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js" integrity="sha256-aB35laj7IZhLTx58xw/Gm1EKOoJJKZt6RY+bH1ReHxs=" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js" integrity="sha256-wouRkivKKXA3y6AuyFwcDcF50alCNV8LbghfYCH6Z98=" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js" integrity="sha256-9hrJxD4IQsWHdNpzLkJKYGiY/SEZFJJSUqyeZPNKd8g=" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.js" integrity="sha256-oeWyQyKKUurcnbFRsfeSgrdOpXXiRYopnPjTVZ+6UmI=" crossorigin="anonymous"></script>
|
|
||||||
</head>
|
|
||||||
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
|
|
||||||
<div id="graphiql" style="height: 100vh;">Loading...</div>
|
|
||||||
<script>
|
|
||||||
function graphQLFetcher(graphQLParams) {
|
|
||||||
return fetch("/plugins/playbooks/api/v0/query", {
|
|
||||||
method: "post",
|
|
||||||
body: JSON.stringify(graphQLParams),
|
|
||||||
credentials: "include",
|
|
||||||
headers: {
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
}
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.text();
|
|
||||||
}).then(function (responseBody) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(responseBody);
|
|
||||||
} catch (error) {
|
|
||||||
return responseBody;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
ReactDOM.render(
|
|
||||||
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
|
|
||||||
document.getElementById("graphiql")
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// statusRecorder intercepts and saves the status code written to an http.ResponseWriter.
|
|
||||||
type statusRecorder struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
statusCode int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *statusRecorder) WriteHeader(code int) {
|
|
||||||
// Forward the write
|
|
||||||
r.ResponseWriter.WriteHeader(code)
|
|
||||||
|
|
||||||
// Save the status code
|
|
||||||
r.statusCode = code
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogRequest logs each request, attaching a unique request_id to the request context to trace
|
|
||||||
// logs throughout the request lifecycle.
|
|
||||||
func LogRequest(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
recorder := statusRecorder{w, 200}
|
|
||||||
requestID := model.NewId()
|
|
||||||
|
|
||||||
startMilis := time.Now().UnixNano() / int64(time.Millisecond)
|
|
||||||
|
|
||||||
logger := logrus.WithFields(logrus.Fields{
|
|
||||||
"method": r.Method,
|
|
||||||
"url": r.URL.String(),
|
|
||||||
"user_id": r.Header.Get("Mattermost-User-Id"),
|
|
||||||
"request_id": requestID,
|
|
||||||
"user_agent": r.Header.Get("User-Agent"),
|
|
||||||
})
|
|
||||||
r = r.WithContext(context.WithValue(r.Context(), requestIDContextKey, requestID))
|
|
||||||
|
|
||||||
logger.Debug("Received HTTP request")
|
|
||||||
|
|
||||||
next.ServeHTTP(&recorder, r)
|
|
||||||
|
|
||||||
gqlOp := r.Header.Get("X-GQL-Operation")
|
|
||||||
if gqlOp != "" {
|
|
||||||
logger = logger.WithField("gql_operation", gqlOp)
|
|
||||||
}
|
|
||||||
|
|
||||||
endMilis := time.Now().UnixNano() / int64(time.Millisecond)
|
|
||||||
logger.WithFields(logrus.Fields{
|
|
||||||
"time": endMilis - startMilis,
|
|
||||||
"status": recorder.statusCode,
|
|
||||||
}).Debug("Handled HTTP request")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,774 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/config"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PlaybookHandler is the API handler.
|
|
||||||
type PlaybookHandler struct {
|
|
||||||
*ErrorHandler
|
|
||||||
playbookService app.PlaybookService
|
|
||||||
api playbooks.ServicesAPI
|
|
||||||
config config.Service
|
|
||||||
permissions *app.PermissionsService
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsKey = "global_settings"
|
|
||||||
const maxPlaybooksToAutocomplete = 15
|
|
||||||
|
|
||||||
// NewPlaybookHandler returns a new playbook api handler
|
|
||||||
func NewPlaybookHandler(router *mux.Router, playbookService app.PlaybookService, api playbooks.ServicesAPI, configService config.Service, permissions *app.PermissionsService) *PlaybookHandler {
|
|
||||||
handler := &PlaybookHandler{
|
|
||||||
ErrorHandler: &ErrorHandler{},
|
|
||||||
playbookService: playbookService,
|
|
||||||
api: api,
|
|
||||||
config: configService,
|
|
||||||
permissions: permissions,
|
|
||||||
}
|
|
||||||
|
|
||||||
playbooksRouter := router.PathPrefix("/playbooks").Subrouter()
|
|
||||||
|
|
||||||
playbooksRouter.HandleFunc("", withContext(handler.createPlaybook)).Methods(http.MethodPost)
|
|
||||||
|
|
||||||
playbooksRouter.HandleFunc("", withContext(handler.getPlaybooks)).Methods(http.MethodGet)
|
|
||||||
playbooksRouter.HandleFunc("/autocomplete", withContext(handler.getPlaybooksAutoComplete)).Methods(http.MethodGet)
|
|
||||||
playbooksRouter.HandleFunc("/import", withContext(handler.importPlaybook)).Methods(http.MethodPost)
|
|
||||||
|
|
||||||
playbookRouter := playbooksRouter.PathPrefix("/{id:[A-Za-z0-9]+}").Subrouter()
|
|
||||||
playbookRouter.HandleFunc("", withContext(handler.getPlaybook)).Methods(http.MethodGet)
|
|
||||||
playbookRouter.HandleFunc("", withContext(handler.updatePlaybook)).Methods(http.MethodPut)
|
|
||||||
playbookRouter.HandleFunc("", withContext(handler.archivePlaybook)).Methods(http.MethodDelete)
|
|
||||||
playbookRouter.HandleFunc("/restore", withContext(handler.restorePlaybook)).Methods(http.MethodPut)
|
|
||||||
playbookRouter.HandleFunc("/export", withContext(handler.exportPlaybook)).Methods(http.MethodGet)
|
|
||||||
playbookRouter.HandleFunc("/duplicate", withContext(handler.duplicatePlaybook)).Methods(http.MethodPost)
|
|
||||||
|
|
||||||
autoFollowsRouter := playbookRouter.PathPrefix("/autofollows").Subrouter()
|
|
||||||
autoFollowsRouter.HandleFunc("", withContext(handler.getAutoFollows)).Methods(http.MethodGet)
|
|
||||||
autoFollowRouter := autoFollowsRouter.PathPrefix("/{userID:[A-Za-z0-9]+}").Subrouter()
|
|
||||||
autoFollowRouter.HandleFunc("", withContext(handler.autoFollow)).Methods(http.MethodPut)
|
|
||||||
autoFollowRouter.HandleFunc("", withContext(handler.autoUnfollow)).Methods(http.MethodDelete)
|
|
||||||
|
|
||||||
insightsRouter := playbooksRouter.PathPrefix("/insights").Subrouter()
|
|
||||||
insightsRouter.HandleFunc("/user/me", withContext(handler.getTopPlaybooksForUser)).Methods(http.MethodGet)
|
|
||||||
insightsRouter.HandleFunc("/teams/{teamID}", withContext(handler.getTopPlaybooksForTeam)).Methods(http.MethodGet)
|
|
||||||
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) validPlaybook(w http.ResponseWriter, logger logrus.FieldLogger, playbook *app.Playbook) bool {
|
|
||||||
if playbook.WebhookOnCreationEnabled {
|
|
||||||
if err := app.ValidateWebhookURLs(playbook.WebhookOnCreationURLs); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, logger, http.StatusBadRequest, err.Error(), err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if playbook.WebhookOnStatusUpdateEnabled {
|
|
||||||
if err := app.ValidateWebhookURLs(playbook.WebhookOnStatusUpdateURLs); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, logger, http.StatusBadRequest, err.Error(), err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if playbook.CategorizeChannelEnabled {
|
|
||||||
if err := app.ValidateCategoryName(playbook.CategoryName); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, logger, http.StatusBadRequest, "invalid category name", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(playbook.SignalAnyKeywords) != 0 {
|
|
||||||
playbook.SignalAnyKeywords = app.ProcessSignalAnyKeywords(playbook.SignalAnyKeywords)
|
|
||||||
}
|
|
||||||
|
|
||||||
if playbook.BroadcastEnabled { //nolint
|
|
||||||
for _, channelID := range playbook.BroadcastChannelIDs {
|
|
||||||
channel, err := h.api.GetChannelByID(channelID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, logger, http.StatusBadRequest, "broadcasting to invalid channel ID", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// check if channel is archived
|
|
||||||
if channel.DeleteAt != 0 {
|
|
||||||
h.HandleErrorWithCode(w, logger, http.StatusBadRequest, "broadcasting to archived channel", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for listIndex := range playbook.Checklists {
|
|
||||||
for itemIndex := range playbook.Checklists[listIndex].Items {
|
|
||||||
if err := validateTaskActions(playbook.Checklists[listIndex].Items[itemIndex].TaskActions); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, logger, http.StatusBadRequest, "invalid task actions", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) createPlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
var playbook app.Playbook
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&playbook); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode playbook", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if playbook.ID != "" {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Playbook given already has ID", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if playbook.ReminderTimerDefaultSeconds <= 0 {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "playbook ReminderTimerDefaultSeconds must be > 0", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookCreate(userID, playbook)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not specified make the creator the sole admin
|
|
||||||
if len(playbook.Members) == 0 {
|
|
||||||
playbook.Members = []app.PlaybookMember{
|
|
||||||
{
|
|
||||||
UserID: userID,
|
|
||||||
Roles: []string{app.PlaybookRoleMember, app.PlaybookRoleAdmin},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.validPlaybook(w, c.logger, &playbook) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.validateMetrics(playbook); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid metrics configs", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
app.CleanUpChecklists(playbook.Checklists)
|
|
||||||
|
|
||||||
if err := validatePreAssignment(playbook); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Invalid pre-assignment", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := h.playbookService.Create(playbook, userID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result := struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}{
|
|
||||||
ID: id,
|
|
||||||
}
|
|
||||||
w.Header().Add("Location", makeAPIURL(h.api, "playbooks/%s", id))
|
|
||||||
|
|
||||||
ReturnJSON(w, &result, http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) getPlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
playbookID := vars["id"]
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(userID, playbookID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
playbook, err := h.playbookService.Get(playbookID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ReturnJSON(w, &playbook, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) updatePlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
var playbook app.Playbook
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&playbook); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode playbook", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force parsed playbook id to be URL parameter id
|
|
||||||
playbook.ID = vars["id"]
|
|
||||||
oldPlaybook, err := h.playbookService.Get(playbook.ID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = h.validateMetrics(playbook); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid metrics configs", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookModifyWithFixes(userID, &playbook, oldPlaybook)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if oldPlaybook.DeleteAt != 0 {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Playbook cannot be modified", fmt.Errorf("playbook with id '%s' cannot be modified because it is archived", playbook.ID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.validPlaybook(w, c.logger, &playbook) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
app.CleanUpChecklists(playbook.Checklists)
|
|
||||||
|
|
||||||
if err = validatePreAssignment(playbook); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Invalid user pre-assignment", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.playbookService.Update(playbook, userID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func validatePreAssignment(pb app.Playbook) error {
|
|
||||||
assignees := app.GetDistinctAssignees(pb.Checklists)
|
|
||||||
return app.ValidatePreAssignment(assignees, pb.InvitedUserIDs, pb.InviteUsersEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateTaskActions validates the taskactions in the given checklist
|
|
||||||
// NOTE: Any changes to this function must be made to function 'validateUpdateTaskActions' for the GraphQL endpoint.
|
|
||||||
func validateTaskActions(taskActions []app.TaskAction) error {
|
|
||||||
for _, ta := range taskActions {
|
|
||||||
if err := app.ValidateTrigger(ta.Trigger); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, a := range ta.Actions {
|
|
||||||
if err := app.ValidateAction(a); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) archivePlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
playbookID := vars["id"]
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
playbookToArchive, err := h.playbookService.Get(playbookID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.DeletePlaybook(userID, playbookToArchive)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.playbookService.Archive(playbookToArchive, userID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) restorePlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
playbookID := vars["id"]
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
playbookToRestore, err := h.playbookService.Get(playbookID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.DeletePlaybook(userID, playbookToRestore)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.playbookService.Restore(playbookToRestore, userID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) getPlaybooks(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
params := r.URL.Query()
|
|
||||||
teamID := params.Get("team_id")
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
opts, err := parseGetPlaybooksOptions(r.URL)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, fmt.Sprintf("failed to get playbooks: %s", err.Error()), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if teamID != "" && !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookList(userID, teamID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
requesterInfo := app.RequesterInfo{
|
|
||||||
UserID: userID,
|
|
||||||
TeamID: teamID,
|
|
||||||
IsAdmin: app.IsSystemAdmin(userID, h.api),
|
|
||||||
}
|
|
||||||
|
|
||||||
isGuest, err := app.IsGuest(userID, h.api)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if isGuest {
|
|
||||||
opts.WithMembershipOnly = true
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookResults, err := h.playbookService.GetPlaybooksForTeam(requesterInfo, teamID, opts)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ReturnJSON(w, playbookResults, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) getPlaybooksAutoComplete(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
query := r.URL.Query()
|
|
||||||
teamID := query.Get("team_id")
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookList(userID, teamID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
requesterInfo := app.RequesterInfo{
|
|
||||||
UserID: userID,
|
|
||||||
TeamID: teamID,
|
|
||||||
IsAdmin: app.IsSystemAdmin(userID, h.api),
|
|
||||||
}
|
|
||||||
|
|
||||||
playbooksResult, err := h.playbookService.GetPlaybooksForTeam(requesterInfo, teamID, app.PlaybookFilterOptions{
|
|
||||||
Page: 0,
|
|
||||||
PerPage: maxPlaybooksToAutocomplete,
|
|
||||||
WithArchived: query.Get("with_archived") == "true",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
list := make([]model.AutocompleteListItem, 0)
|
|
||||||
|
|
||||||
for _, playbook := range playbooksResult.Items {
|
|
||||||
list = append(list, model.AutocompleteListItem{
|
|
||||||
Item: playbook.ID,
|
|
||||||
HelpText: playbook.Title,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ReturnJSON(w, list, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseGetPlaybooksOptions(u *url.URL) (app.PlaybookFilterOptions, error) {
|
|
||||||
params := u.Query()
|
|
||||||
|
|
||||||
var sortField app.SortField
|
|
||||||
param := strings.ToLower(params.Get("sort"))
|
|
||||||
switch param {
|
|
||||||
case "title", "":
|
|
||||||
sortField = app.SortByTitle
|
|
||||||
case "stages":
|
|
||||||
sortField = app.SortByStages
|
|
||||||
case "steps":
|
|
||||||
sortField = app.SortBySteps
|
|
||||||
case "runs":
|
|
||||||
sortField = app.SortByRuns
|
|
||||||
case "last_run_at":
|
|
||||||
sortField = app.SortByLastRunAt
|
|
||||||
case "active_runs":
|
|
||||||
sortField = app.SortByActiveRuns
|
|
||||||
default:
|
|
||||||
return app.PlaybookFilterOptions{}, errors.Errorf("bad parameter 'sort' (%s): it should be empty or one of 'title', 'stages', 'steps', 'runs', 'last_run_at'", param)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sortDirection app.SortDirection
|
|
||||||
param = strings.ToLower(params.Get("direction"))
|
|
||||||
switch param {
|
|
||||||
case "asc", "":
|
|
||||||
sortDirection = app.DirectionAsc
|
|
||||||
case "desc":
|
|
||||||
sortDirection = app.DirectionDesc
|
|
||||||
default:
|
|
||||||
return app.PlaybookFilterOptions{}, errors.Errorf("bad parameter 'direction' (%s): it should be empty or one of 'asc' or 'desc'", param)
|
|
||||||
}
|
|
||||||
|
|
||||||
pageParam := params.Get("page")
|
|
||||||
if pageParam == "" {
|
|
||||||
pageParam = "0"
|
|
||||||
}
|
|
||||||
page, err := strconv.Atoi(pageParam)
|
|
||||||
if err != nil {
|
|
||||||
return app.PlaybookFilterOptions{}, errors.Wrapf(err, "bad parameter 'page': it should be a number")
|
|
||||||
}
|
|
||||||
if page < 0 {
|
|
||||||
return app.PlaybookFilterOptions{}, errors.Errorf("bad parameter 'page': it should be a positive number")
|
|
||||||
}
|
|
||||||
|
|
||||||
perPageParam := params.Get("per_page")
|
|
||||||
if perPageParam == "" || perPageParam == "0" {
|
|
||||||
perPageParam = "1000"
|
|
||||||
}
|
|
||||||
perPage, err := strconv.Atoi(perPageParam)
|
|
||||||
if err != nil {
|
|
||||||
return app.PlaybookFilterOptions{}, errors.Wrapf(err, "bad parameter 'per_page': it should be a number")
|
|
||||||
}
|
|
||||||
if perPage < 0 {
|
|
||||||
return app.PlaybookFilterOptions{}, errors.Errorf("bad parameter 'per_page': it should be a positive number")
|
|
||||||
}
|
|
||||||
|
|
||||||
searchTerm := u.Query().Get("search_term")
|
|
||||||
|
|
||||||
withArchived, _ := strconv.ParseBool(u.Query().Get("with_archived"))
|
|
||||||
|
|
||||||
return app.PlaybookFilterOptions{
|
|
||||||
Sort: sortField,
|
|
||||||
Direction: sortDirection,
|
|
||||||
Page: page,
|
|
||||||
PerPage: perPage,
|
|
||||||
SearchTerm: searchTerm,
|
|
||||||
WithArchived: withArchived,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) autoFollow(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
playbookID := mux.Vars(r)["id"]
|
|
||||||
currentUserID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
userID := mux.Vars(r)["userID"]
|
|
||||||
|
|
||||||
if currentUserID != userID && !app.IsSystemAdmin(currentUserID, h.api) {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "User doesn't have permissions to make another user autofollow the playbook.", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(userID, playbookID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.playbookService.AutoFollow(playbookID, userID); err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) autoUnfollow(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
playbookID := mux.Vars(r)["id"]
|
|
||||||
currentUserID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
userID := mux.Vars(r)["userID"]
|
|
||||||
|
|
||||||
if currentUserID != userID && !app.IsSystemAdmin(currentUserID, h.api) {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "User doesn't have permissions to make another user autofollow the playbook.", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(userID, playbookID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.playbookService.AutoUnfollow(playbookID, userID); err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getAutoFollows returns the list of users that have marked this playbook for auto-following runs
|
|
||||||
func (h *PlaybookHandler) getAutoFollows(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
playbookID := mux.Vars(r)["id"]
|
|
||||||
currentUserID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(currentUserID, playbookID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
autoFollowers, err := h.playbookService.GetAutoFollows(playbookID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ReturnJSON(w, autoFollowers, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) exportPlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
playbookID := vars["id"]
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
playbook, err := h.playbookService.Get(playbookID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookViewWithPlaybook(userID, playbook)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
export, err := app.GeneratePlaybookExport(playbook)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_, _ = w.Write(export)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) duplicatePlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
playbookID := vars["id"]
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
playbook, err := h.playbookService.Get(playbookID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookViewWithPlaybook(userID, playbook)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookCreate(userID, playbook)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newPlaybookID, err := h.playbookService.Duplicate(playbook, userID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result := struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}{
|
|
||||||
ID: newPlaybookID,
|
|
||||||
}
|
|
||||||
ReturnJSON(w, &result, http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) importPlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
params := r.URL.Query()
|
|
||||||
teamID := params.Get("team_id")
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
var importBlock struct {
|
|
||||||
app.Playbook
|
|
||||||
Version int `json:"version"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&importBlock); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode playbook import", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
playbook := importBlock.Playbook
|
|
||||||
|
|
||||||
if playbook.ID != "" {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "playbook import should not have ID field", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if importBlock.Version != app.CurrentPlaybookExportVersion {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Unsupported import version", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make the importer the sole admin of the playbook.
|
|
||||||
playbook.Members = []app.PlaybookMember{
|
|
||||||
{
|
|
||||||
UserID: userID,
|
|
||||||
Roles: []string{app.PlaybookRoleMember, app.PlaybookRoleAdmin},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force the imported playbook to be public to avoid licencing issues
|
|
||||||
playbook.Public = true
|
|
||||||
|
|
||||||
if teamID != "" {
|
|
||||||
playbook.TeamID = teamID
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookCreate(userID, playbook)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.validPlaybook(w, c.logger, &playbook) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := h.playbookService.Import(playbook, userID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result := struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}{
|
|
||||||
ID: id,
|
|
||||||
}
|
|
||||||
w.Header().Add("Location", makeAPIURL(h.api, "playbooks/%s", id))
|
|
||||||
|
|
||||||
ReturnJSON(w, &result, http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) validateMetrics(pb app.Playbook) error {
|
|
||||||
if len(pb.Metrics) > app.MaxMetricsPerPlaybook {
|
|
||||||
return errors.Errorf(fmt.Sprintf("playbook cannot have more than %d key metrics", app.MaxMetricsPerPlaybook))
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if titles are unique
|
|
||||||
titles := make(map[string]bool)
|
|
||||||
for _, m := range pb.Metrics {
|
|
||||||
if titles[m.Title] {
|
|
||||||
return errors.Errorf("metrics names must be unique")
|
|
||||||
}
|
|
||||||
titles[m.Title] = true
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) getTopPlaybooksForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
params := r.URL.Query()
|
|
||||||
timeRange := params.Get("time_range")
|
|
||||||
teamID := params.Get("team_id")
|
|
||||||
if teamID == "" {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusNotImplemented, "invalid team_id parameter", errors.New("teamID cannot be empty"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookList(userID, teamID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
page, err := strconv.Atoi(params.Get("page"))
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "error converting page parameter to integer", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
perPage, err := strconv.Atoi(params.Get("per_page"))
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "error converting per_page parameter to integer", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// setting startTime as per user's location
|
|
||||||
user, err := h.api.GetUserByID(userID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to get user", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
timezone := user.GetTimezoneLocation()
|
|
||||||
|
|
||||||
// get unix time for duration
|
|
||||||
startTime, appErr := model.GetStartOfDayForTimeRange(timeRange, timezone)
|
|
||||||
if appErr != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid time parameter", appErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
topPlaybooks, err := h.playbookService.GetTopPlaybooksForUser(teamID, userID, &model.InsightsOpts{
|
|
||||||
StartUnixMilli: model.GetMillisForTime(*startTime),
|
|
||||||
Page: page,
|
|
||||||
PerPage: perPage,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ReturnJSON(w, &topPlaybooks, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *PlaybookHandler) getTopPlaybooksForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
teamID := vars["teamID"]
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
params := r.URL.Query()
|
|
||||||
timeRange := params.Get("time_range")
|
|
||||||
if teamID == "" {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusNotImplemented, "invalid team_id parameter", errors.New("teamID cannot be empty"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookList(userID, teamID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
page, err := strconv.Atoi(params.Get("page"))
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "error converting page parameter to integer", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
perPage, err := strconv.Atoi(params.Get("per_page"))
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "error converting per_page parameter to integer", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// setting startTime as per user's location
|
|
||||||
user, err := h.api.GetUserByID(userID)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to get user", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
timezone := user.GetTimezoneLocation()
|
|
||||||
|
|
||||||
// get unix time for duration
|
|
||||||
startTime, appErr := model.GetStartOfDayForTimeRange(timeRange, timezone)
|
|
||||||
if appErr != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid time parameter", appErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
topPlaybooks, err := h.playbookService.GetTopPlaybooksForTeam(teamID, userID, &model.InsightsOpts{
|
|
||||||
StartUnixMilli: model.GetMillisForTime(*startTime),
|
|
||||||
Page: page,
|
|
||||||
PerPage: perPage,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ReturnJSON(w, &topPlaybooks, http.StatusOK)
|
|
||||||
}
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
type Query {
|
|
||||||
playbook(id: String!): Playbook
|
|
||||||
playbooks(
|
|
||||||
teamID: String = "",
|
|
||||||
sort: String = "title",
|
|
||||||
direction: String = "ASC",
|
|
||||||
searchTerm: String = "",
|
|
||||||
withArchived: Boolean = false,
|
|
||||||
withMembershipOnly: Boolean = false,
|
|
||||||
): [Playbook!]!
|
|
||||||
|
|
||||||
run(id: String!): Run
|
|
||||||
runs(
|
|
||||||
teamID: String = "",
|
|
||||||
sort: String = "",
|
|
||||||
direction: String = "",
|
|
||||||
statuses: [String!] = [],
|
|
||||||
participantOrFollowerID: String = "",
|
|
||||||
channelID: String = "",
|
|
||||||
first: Int,
|
|
||||||
after: String,
|
|
||||||
types: [PlaybookRunType!] = [],
|
|
||||||
): RunConnection!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Mutation {
|
|
||||||
updatePlaybookFavorite(id: String!, favorite: Boolean!): String!
|
|
||||||
updatePlaybook(id: String!, updates: PlaybookUpdates!): String!
|
|
||||||
|
|
||||||
addMetric(playbookID: String!, title: String!, description: String!, type: String!, target: Int): String!
|
|
||||||
updateMetric(id: String!, title: String, description: String, target: Int): String!
|
|
||||||
deleteMetric(id: String!): String!
|
|
||||||
|
|
||||||
addPlaybookMember(playbookID: String!, userID: String!): String!
|
|
||||||
removePlaybookMember(playbookID: String!, userID: String!): String!
|
|
||||||
|
|
||||||
setRunFavorite(id: String!, fav: Boolean!): String!
|
|
||||||
updateRun(id: String!, updates: RunUpdates!): String!
|
|
||||||
addRunParticipants(runID: String!, userIDs: [String!]!, forceAddToChannel: Boolean = false): String!
|
|
||||||
removeRunParticipants(runID: String!, userIDs: [String!]!): String!
|
|
||||||
changeRunOwner(runID: String!, ownerID: String!): String!
|
|
||||||
updateRunTaskActions(runID: String!, checklistNum: Float!, itemNum: Float!, taskActions: [TaskActionUpdates!]): String!
|
|
||||||
}
|
|
||||||
|
|
||||||
type PageInfo {
|
|
||||||
hasNextPage: Boolean!
|
|
||||||
startCursor: String!
|
|
||||||
endCursor: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
input PlaybookUpdates {
|
|
||||||
title: String
|
|
||||||
description: String
|
|
||||||
public: Boolean
|
|
||||||
createPublicPlaybookRun: Boolean
|
|
||||||
reminderMessageTemplate: String
|
|
||||||
reminderTimerDefaultSeconds: Float
|
|
||||||
statusUpdateEnabled: Boolean
|
|
||||||
invitedUserIDs: [String!]
|
|
||||||
invitedGroupIDs: [String!]
|
|
||||||
inviteUsersEnabled: Boolean
|
|
||||||
defaultOwnerID: String
|
|
||||||
defaultOwnerEnabled: Boolean
|
|
||||||
broadcastChannelIDs: [String!]
|
|
||||||
broadcastEnabled: Boolean
|
|
||||||
webhookOnCreationURLs: [String!]
|
|
||||||
webhookOnCreationEnabled: Boolean
|
|
||||||
messageOnJoin: String
|
|
||||||
messageOnJoinEnabled: Boolean
|
|
||||||
retrospectiveReminderIntervalSeconds: Float
|
|
||||||
retrospectiveTemplate: String
|
|
||||||
retrospectiveEnabled: Boolean
|
|
||||||
webhookOnStatusUpdateURLs: [String!]
|
|
||||||
webhookOnStatusUpdateEnabled: Boolean
|
|
||||||
signalAnyKeywords: [String!]
|
|
||||||
signalAnyKeywordsEnabled: Boolean
|
|
||||||
categorizeChannelEnabled: Boolean
|
|
||||||
categoryName: String
|
|
||||||
runSummaryTemplateEnabled: Boolean
|
|
||||||
runSummaryTemplate: String
|
|
||||||
channelNameTemplate: String
|
|
||||||
checklists: [ChecklistUpdates!]
|
|
||||||
createChannelMemberOnNewParticipant: Boolean
|
|
||||||
removeChannelMemberOnRemovedParticipant: Boolean
|
|
||||||
channelId: String
|
|
||||||
channelMode: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input ChecklistUpdates {
|
|
||||||
title: String!
|
|
||||||
items: [ChecklistItemUpdates!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
input ChecklistItemUpdates {
|
|
||||||
title: String!
|
|
||||||
description: String!
|
|
||||||
state: String!
|
|
||||||
stateModified: Float!
|
|
||||||
assigneeID: String!
|
|
||||||
assigneeModified: Float!
|
|
||||||
command: String!
|
|
||||||
commandLastRun: Float!
|
|
||||||
dueDate: Float!
|
|
||||||
taskActions: [TaskActionUpdates!]
|
|
||||||
}
|
|
||||||
|
|
||||||
input TaskActionUpdates {
|
|
||||||
trigger: TriggerUpdates!
|
|
||||||
actions: [ActionUpdates!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
input TriggerUpdates {
|
|
||||||
type: String!
|
|
||||||
payload: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
input ActionUpdates {
|
|
||||||
type: String!
|
|
||||||
payload: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Playbook {
|
|
||||||
id: String!
|
|
||||||
title: String!
|
|
||||||
description: String!
|
|
||||||
teamID: String!
|
|
||||||
createPublicPlaybookRun: Boolean!
|
|
||||||
deleteAt: Float!
|
|
||||||
lastRunAt: Float!
|
|
||||||
numRuns: Int!
|
|
||||||
activeRuns: Int!
|
|
||||||
runSummaryTemplateEnabled: Boolean!
|
|
||||||
defaultPlaybookMemberRole: String!
|
|
||||||
public: Boolean!
|
|
||||||
checklists: [Checklist!]!
|
|
||||||
members: [Member!]!
|
|
||||||
reminderMessageTemplate: String!
|
|
||||||
reminderTimerDefaultSeconds: Float!
|
|
||||||
statusUpdateEnabled: Boolean!
|
|
||||||
invitedUserIDs: [String!]!
|
|
||||||
invitedGroupIDs: [String!]!
|
|
||||||
inviteUsersEnabled: Boolean!
|
|
||||||
defaultOwnerID: String!
|
|
||||||
defaultOwnerEnabled: Boolean!
|
|
||||||
broadcastChannelIDs: [String!]!
|
|
||||||
broadcastEnabled: Boolean!
|
|
||||||
webhookOnCreationURLs: [String!]!
|
|
||||||
webhookOnCreationEnabled: Boolean!
|
|
||||||
messageOnJoin: String!
|
|
||||||
messageOnJoinEnabled: Boolean!
|
|
||||||
retrospectiveReminderIntervalSeconds: Float!
|
|
||||||
retrospectiveTemplate: String!
|
|
||||||
retrospectiveEnabled: Boolean!
|
|
||||||
webhookOnStatusUpdateURLs: [String!]!
|
|
||||||
webhookOnStatusUpdateEnabled: Boolean!
|
|
||||||
signalAnyKeywords: [String!]!
|
|
||||||
signalAnyKeywordsEnabled: Boolean!
|
|
||||||
categorizeChannelEnabled: Boolean!
|
|
||||||
categoryName: String!
|
|
||||||
runSummaryTemplateEnabled: Boolean!
|
|
||||||
runSummaryTemplate: String!
|
|
||||||
channelNameTemplate: String!
|
|
||||||
defaultPlaybookAdminRole: String!
|
|
||||||
defaultPlaybookMemberRole: String!
|
|
||||||
defaultRunAdminRole: String!
|
|
||||||
defaultRunMemberRole: String!
|
|
||||||
metrics: [PlaybookMetricConfig!]!
|
|
||||||
isFavorite: Boolean!
|
|
||||||
createChannelMemberOnNewParticipant: Boolean!
|
|
||||||
removeChannelMemberOnRemovedParticipant: Boolean!
|
|
||||||
channelID: String!
|
|
||||||
channelMode: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Checklist {
|
|
||||||
title: String!
|
|
||||||
items: [ChecklistItem!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Member {
|
|
||||||
userID: String!
|
|
||||||
roles: [String!]!
|
|
||||||
schemeRoles: [String!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChecklistItem {
|
|
||||||
title: String!
|
|
||||||
description: String!
|
|
||||||
state: String!
|
|
||||||
stateModified: Float!
|
|
||||||
assigneeID: String!
|
|
||||||
assigneeModified: Float!
|
|
||||||
command: String!
|
|
||||||
commandLastRun: Float!
|
|
||||||
dueDate: Float!
|
|
||||||
taskActions: [TaskAction!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaskAction {
|
|
||||||
trigger: Trigger!
|
|
||||||
actions: [Action!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Trigger {
|
|
||||||
type: String!
|
|
||||||
payload: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Action {
|
|
||||||
type: String!
|
|
||||||
payload: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
enum MetricType {
|
|
||||||
metric_duration
|
|
||||||
metric_currency
|
|
||||||
metric_integer
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlaybookMetricConfig {
|
|
||||||
id: String!
|
|
||||||
title: String!
|
|
||||||
description: String!
|
|
||||||
type: MetricType!
|
|
||||||
target: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PlaybookRunType {
|
|
||||||
playbook
|
|
||||||
channelChecklist
|
|
||||||
}
|
|
||||||
|
|
||||||
type Run {
|
|
||||||
id: String!
|
|
||||||
playbookID: String!
|
|
||||||
playbook: Playbook
|
|
||||||
name: String!
|
|
||||||
ownerUserID: String!
|
|
||||||
channelID: String!
|
|
||||||
postID: String!
|
|
||||||
teamID: String!
|
|
||||||
isFavorite: Boolean!
|
|
||||||
currentStatus: String!
|
|
||||||
createAt: Float!
|
|
||||||
endAt: Float!
|
|
||||||
participantIDs: [String!]!
|
|
||||||
|
|
||||||
summary: String!
|
|
||||||
summaryModifiedAt: Float!
|
|
||||||
checklists: [Checklist!]!
|
|
||||||
|
|
||||||
retrospective: String!
|
|
||||||
retrospectivePublishedAt: Float!
|
|
||||||
retrospectiveReminderIntervalSeconds: Float!
|
|
||||||
retrospectiveEnabled: Boolean!
|
|
||||||
retrospectiveWasCanceled: Boolean!
|
|
||||||
|
|
||||||
statusUpdateEnabled: Boolean!
|
|
||||||
statusUpdateBroadcastWebhooksEnabled: Boolean!
|
|
||||||
lastStatusUpdateAt: Float!
|
|
||||||
statusPosts: [StatusPost!]!
|
|
||||||
reminderPostId: String!
|
|
||||||
reminderMessageTemplate: String!
|
|
||||||
reminderTimerDefaultSeconds: Float!
|
|
||||||
previousReminder: Float!
|
|
||||||
|
|
||||||
statusUpdateBroadcastChannelsEnabled: Boolean!
|
|
||||||
statusUpdateBroadcastWebhooksEnabled: Boolean!
|
|
||||||
broadcastChannelIDs: [String!]!
|
|
||||||
webhookOnStatusUpdateURLs: [String!]!
|
|
||||||
createChannelMemberOnNewParticipant: Boolean!
|
|
||||||
removeChannelMemberOnRemovedParticipant: Boolean!
|
|
||||||
|
|
||||||
lastUpdatedAt: Float!
|
|
||||||
|
|
||||||
timelineEvents: [TimelineEvent!]!
|
|
||||||
followers: [String!]!
|
|
||||||
|
|
||||||
numTasks: Int!
|
|
||||||
numTasksClosed: Int!
|
|
||||||
|
|
||||||
type: PlaybookRunType!
|
|
||||||
}
|
|
||||||
|
|
||||||
type RunConnection {
|
|
||||||
totalCount: Int!
|
|
||||||
edges: [RunEdge!]!
|
|
||||||
pageInfo: PageInfo!
|
|
||||||
}
|
|
||||||
|
|
||||||
type RunEdge {
|
|
||||||
cursor: String!
|
|
||||||
node: Run!
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatusPost {
|
|
||||||
id: String!
|
|
||||||
createAt: Float!
|
|
||||||
deleteAt: Float!
|
|
||||||
}
|
|
||||||
|
|
||||||
type TimelineEvent {
|
|
||||||
id: String!
|
|
||||||
createAt: Float!
|
|
||||||
deleteAt: Float!
|
|
||||||
eventType: String!
|
|
||||||
details: String!
|
|
||||||
postID: String!
|
|
||||||
summary: String!
|
|
||||||
subjectUserID: String!
|
|
||||||
creatorUserID: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
input RunUpdates {
|
|
||||||
name: String
|
|
||||||
summary: String
|
|
||||||
createChannelMemberOnNewParticipant: Boolean
|
|
||||||
removeChannelMemberOnRemovedParticipant: Boolean
|
|
||||||
statusUpdateBroadcastChannelsEnabled: Boolean
|
|
||||||
statusUpdateBroadcastWebhooksEnabled: Boolean
|
|
||||||
broadcastChannelIDs: [String!]
|
|
||||||
webhookOnStatusUpdateURLs: [String!]
|
|
||||||
channelID: String
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/client"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/config"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SettingsHandler is the API handler.
|
|
||||||
type SettingsHandler struct {
|
|
||||||
*ErrorHandler
|
|
||||||
api playbooks.ServicesAPI
|
|
||||||
config config.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSettingsHandler returns a new settings api handler
|
|
||||||
func NewSettingsHandler(router *mux.Router, api playbooks.ServicesAPI, configService config.Service) *SettingsHandler {
|
|
||||||
handler := &SettingsHandler{
|
|
||||||
ErrorHandler: &ErrorHandler{},
|
|
||||||
api: api,
|
|
||||||
config: configService,
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsRouter := router.PathPrefix("/settings").Subrouter()
|
|
||||||
settingsRouter.HandleFunc("", handler.getSettings).Methods(http.MethodGet)
|
|
||||||
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *SettingsHandler) getSettings(w http.ResponseWriter, r *http.Request) {
|
|
||||||
cfg := h.config.GetConfiguration()
|
|
||||||
settings := client.GlobalSettings{
|
|
||||||
EnableExperimentalFeatures: cfg.EnableExperimentalFeatures,
|
|
||||||
}
|
|
||||||
|
|
||||||
ReturnJSON(w, &settings, http.StatusOK)
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SignalHandler struct {
|
|
||||||
*ErrorHandler
|
|
||||||
api playbooks.ServicesAPI
|
|
||||||
playbookRunService app.PlaybookRunService
|
|
||||||
playbookService app.PlaybookService
|
|
||||||
keywordsThreadIgnorer app.KeywordsThreadIgnorer
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSignalHandler(router *mux.Router, api playbooks.ServicesAPI, playbookRunService app.PlaybookRunService, playbookService app.PlaybookService, keywordsThreadIgnorer app.KeywordsThreadIgnorer) *SignalHandler {
|
|
||||||
handler := &SignalHandler{
|
|
||||||
ErrorHandler: &ErrorHandler{},
|
|
||||||
api: api,
|
|
||||||
playbookRunService: playbookRunService,
|
|
||||||
playbookService: playbookService,
|
|
||||||
keywordsThreadIgnorer: keywordsThreadIgnorer,
|
|
||||||
}
|
|
||||||
|
|
||||||
signalRouter := router.PathPrefix("/signal").Subrouter()
|
|
||||||
|
|
||||||
keywordsRouter := signalRouter.PathPrefix("/keywords").Subrouter()
|
|
||||||
keywordsRouter.HandleFunc("/run-playbook", withContext(handler.playbookRun)).Methods(http.MethodPost)
|
|
||||||
keywordsRouter.HandleFunc("/ignore-thread", withContext(handler.ignoreKeywords)).Methods(http.MethodPost)
|
|
||||||
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *SignalHandler) playbookRun(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
publicErrorMessage := "unable to decode post action integration request"
|
|
||||||
|
|
||||||
var req *model.PostActionIntegrationRequest
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
if err != nil {
|
|
||||||
h.returnError(publicErrorMessage, err, c.logger, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if req == nil {
|
|
||||||
h.returnError(publicErrorMessage, errors.New("nil request"), c.logger, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := getStringField("selected_option", req.Context, w)
|
|
||||||
if err != nil {
|
|
||||||
h.returnError(publicErrorMessage, err, c.logger, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pbook, err := h.playbookService.Get(id)
|
|
||||||
if err != nil {
|
|
||||||
h.returnError("can't get chosen playbook", errors.Wrapf(err, "can't get chosen playbook, id - %s", id), c.logger, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.playbookRunService.OpenCreatePlaybookRunDialog(req.TeamId, req.UserId, req.TriggerId, "", "", []app.Playbook{pbook}); err != nil {
|
|
||||||
h.returnError("can't open dialog", errors.Wrap(err, "can't open a dialog"), c.logger, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ReturnJSON(w, &model.PostActionIntegrationResponse{}, http.StatusOK)
|
|
||||||
if _, err := h.api.DeletePost(req.PostId); err != nil {
|
|
||||||
h.returnError("unable to delete original post", err, c.logger, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *SignalHandler) ignoreKeywords(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
publicErrorMessage := "unable to decode post action integration request"
|
|
||||||
|
|
||||||
var req *model.PostActionIntegrationRequest
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
if err != nil || req == nil {
|
|
||||||
h.returnError(publicErrorMessage, err, c.logger, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
postID, err := getStringField("postID", req.Context, w)
|
|
||||||
if err != nil {
|
|
||||||
h.returnError(publicErrorMessage, err, c.logger, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
post, err := h.api.GetPost(postID)
|
|
||||||
if err != nil {
|
|
||||||
h.returnError(publicErrorMessage, err, c.logger, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.keywordsThreadIgnorer.Ignore(postID, post.UserId)
|
|
||||||
if post.RootId != "" {
|
|
||||||
h.keywordsThreadIgnorer.Ignore(post.RootId, post.UserId)
|
|
||||||
}
|
|
||||||
|
|
||||||
ReturnJSON(w, &model.PostActionIntegrationResponse{}, http.StatusOK)
|
|
||||||
if _, err := h.api.DeletePost(req.PostId); err != nil {
|
|
||||||
h.returnError("unable to delete original post", err, c.logger, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *SignalHandler) returnError(returnMessage string, err error, logger logrus.FieldLogger, w http.ResponseWriter) {
|
|
||||||
resp := model.PostActionIntegrationResponse{
|
|
||||||
EphemeralText: fmt.Sprintf("Error: %s", returnMessage),
|
|
||||||
}
|
|
||||||
logger.WithError(err).Warn(returnMessage)
|
|
||||||
ReturnJSON(w, &resp, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getStringField(field string, context map[string]interface{}, w http.ResponseWriter) (string, error) {
|
|
||||||
fieldInt, ok := context[field]
|
|
||||||
if !ok {
|
|
||||||
return "", errors.Errorf("no %s field in the request context", field)
|
|
||||||
}
|
|
||||||
fieldValue, ok := fieldInt.(string)
|
|
||||||
if !ok {
|
|
||||||
return "", errors.Errorf("%s field is not a string", field)
|
|
||||||
}
|
|
||||||
return fieldValue, nil
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
"gopkg.in/guregu/null.v4"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/sqlstore"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StatsHandler struct {
|
|
||||||
*ErrorHandler
|
|
||||||
api playbooks.ServicesAPI
|
|
||||||
statsStore *sqlstore.StatsStore
|
|
||||||
playbookService app.PlaybookService
|
|
||||||
permissions *app.PermissionsService
|
|
||||||
licenseChecker app.LicenseChecker
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStatsHandler(router *mux.Router, api playbooks.ServicesAPI, statsStore *sqlstore.StatsStore, playbookService app.PlaybookService, permissions *app.PermissionsService, licenseChecker app.LicenseChecker) *StatsHandler {
|
|
||||||
handler := &StatsHandler{
|
|
||||||
ErrorHandler: &ErrorHandler{},
|
|
||||||
api: api,
|
|
||||||
statsStore: statsStore,
|
|
||||||
playbookService: playbookService,
|
|
||||||
permissions: permissions,
|
|
||||||
licenseChecker: licenseChecker,
|
|
||||||
}
|
|
||||||
|
|
||||||
statsRouter := router.PathPrefix("/stats").Subrouter()
|
|
||||||
statsRouter.HandleFunc("/site", withContext(handler.playbookSiteStats)).Methods(http.MethodGet)
|
|
||||||
statsRouter.HandleFunc("/playbook", withContext(handler.playbookStats)).Methods(http.MethodGet)
|
|
||||||
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlaybookStats struct {
|
|
||||||
RunsInProgress int `json:"runs_in_progress"`
|
|
||||||
ParticipantsActive int `json:"participants_active"`
|
|
||||||
RunsFinishedPrev30Days int `json:"runs_finished_prev_30_days"`
|
|
||||||
RunsFinishedPercentageChange int `json:"runs_finished_percentage_change"`
|
|
||||||
RunsStartedPerWeek []int `json:"runs_started_per_week"`
|
|
||||||
RunsStartedPerWeekTimes [][]int64 `json:"runs_started_per_week_times"`
|
|
||||||
ActiveRunsPerDay []int `json:"active_runs_per_day"`
|
|
||||||
ActiveRunsPerDayTimes [][]int64 `json:"active_runs_per_day_times"`
|
|
||||||
ActiveParticipantsPerDay []int `json:"active_participants_per_day"`
|
|
||||||
ActiveParticipantsPerDayTimes [][]int64 `json:"active_participants_per_day_times"`
|
|
||||||
MetricOverallAverage []null.Int `json:"metric_overall_average"`
|
|
||||||
MetricRollingAverage []null.Int `json:"metric_rolling_average"`
|
|
||||||
MetricRollingAverageChange []null.Int `json:"metric_rolling_average_change"`
|
|
||||||
MetricValueRange [][]int64 `json:"metric_value_range"`
|
|
||||||
MetricRollingValues [][]int64 `json:"metric_rolling_values"`
|
|
||||||
LastXRunNames []string `json:"last_x_run_names"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
MetricChartPeriod = 10
|
|
||||||
MetricRollingAveragePeriod = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
func parsePlaybookStatsFilters(u *url.URL) (*sqlstore.StatsFilters, error) {
|
|
||||||
playbookID := u.Query().Get("playbook_id")
|
|
||||||
if playbookID == "" {
|
|
||||||
return nil, errors.New("bad parameter 'playbook_id'; 'playbook_id' is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &sqlstore.StatsFilters{
|
|
||||||
PlaybookID: playbookID,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// playbookStats handles the internal plugin stats
|
|
||||||
func (h *StatsHandler) playbookStats(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.licenseChecker.StatsAllowed() {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "timeline feature is not covered by current server license", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
filters, err := parsePlaybookStatsFilters(r.URL)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Bad filters", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(userID, filters.PlaybookID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
runsFinishedLast30Days := h.statsStore.RunsFinishedBetweenDays(filters, 30, 0)
|
|
||||||
runsFinishedBetween60and30DaysAgo := h.statsStore.RunsFinishedBetweenDays(filters, 60, 31)
|
|
||||||
var percentageChange int
|
|
||||||
if runsFinishedBetween60and30DaysAgo == 0 {
|
|
||||||
percentageChange = 99999999
|
|
||||||
} else {
|
|
||||||
percentageChange = int(math.Floor(float64((runsFinishedLast30Days-runsFinishedBetween60and30DaysAgo)/runsFinishedBetween60and30DaysAgo) * 100))
|
|
||||||
}
|
|
||||||
runsStartedPerWeek, runsStartedPerWeekTimes := h.statsStore.RunsStartedPerWeekLastXWeeks(12, filters)
|
|
||||||
activeRunsPerDay, activeRunsPerDayTimes := h.statsStore.ActiveRunsPerDayLastXDays(14, filters)
|
|
||||||
activeParticipantsPerDay, activeParticipantsPerDayTimes := h.statsStore.ActiveParticipantsPerDayLastXDays(14, filters)
|
|
||||||
|
|
||||||
metricOverallAverage := h.statsStore.MetricOverallAverage(*filters)
|
|
||||||
metricRollingValues, lastXRunNames := h.statsStore.MetricRollingValuesLastXRuns(MetricChartPeriod, 0, *filters)
|
|
||||||
metricRollingAverage, metricRollingAverageChange := h.statsStore.MetricRollingAverageAndChange(MetricRollingAveragePeriod, *filters)
|
|
||||||
metricValueRange := h.statsStore.MetricValueRange(*filters)
|
|
||||||
|
|
||||||
ReturnJSON(w, &PlaybookStats{
|
|
||||||
RunsInProgress: h.statsStore.TotalInProgressPlaybookRuns(filters),
|
|
||||||
ParticipantsActive: h.statsStore.TotalActiveParticipants(filters),
|
|
||||||
RunsFinishedPrev30Days: runsFinishedLast30Days,
|
|
||||||
RunsFinishedPercentageChange: percentageChange,
|
|
||||||
RunsStartedPerWeek: runsStartedPerWeek,
|
|
||||||
RunsStartedPerWeekTimes: runsStartedPerWeekTimes,
|
|
||||||
ActiveRunsPerDay: activeRunsPerDay,
|
|
||||||
ActiveRunsPerDayTimes: activeRunsPerDayTimes,
|
|
||||||
ActiveParticipantsPerDay: activeParticipantsPerDay,
|
|
||||||
ActiveParticipantsPerDayTimes: activeParticipantsPerDayTimes,
|
|
||||||
MetricOverallAverage: metricOverallAverage,
|
|
||||||
MetricRollingValues: metricRollingValues,
|
|
||||||
MetricValueRange: metricValueRange,
|
|
||||||
MetricRollingAverage: metricRollingAverage,
|
|
||||||
MetricRollingAverageChange: metricRollingAverageChange,
|
|
||||||
LastXRunNames: lastXRunNames,
|
|
||||||
}, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlaybookSiteStats struct {
|
|
||||||
TotalPlaybooks int `json:"total_playbooks"`
|
|
||||||
TotalPlaybookRuns int `json:"total_playbook_runs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// playbooSitekStats collects and sends the stats used for system-console > statistics
|
|
||||||
//
|
|
||||||
// Response 200: PlaybookSiteStats
|
|
||||||
// Response 401: when user is not authenticated
|
|
||||||
// Response 403: when user has no permissions to see stats
|
|
||||||
func (h *StatsHandler) playbookSiteStats(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
// user must have right to access analytics
|
|
||||||
if !h.api.HasPermissionTo(userID, model.PermissionGetAnalytics) {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "user is not allowed to get site stats", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
totalPlaybooks, err := h.statsStore.TotalPlaybooks()
|
|
||||||
if err != nil {
|
|
||||||
c.logger.WithError(err).Warn("playbookSiteStats failed fetching total playbooks")
|
|
||||||
}
|
|
||||||
totalRuns, err := h.statsStore.TotalPlaybookRuns()
|
|
||||||
if err != nil {
|
|
||||||
c.logger.WithError(err).Warn("playbookSiteStats failed fetching total playbook runs")
|
|
||||||
}
|
|
||||||
ReturnJSON(w, &PlaybookSiteStats{
|
|
||||||
TotalPlaybooks: totalPlaybooks,
|
|
||||||
TotalPlaybookRuns: totalRuns,
|
|
||||||
}, http.StatusOK)
|
|
||||||
}
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/bot"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TelemetryHandler is the API handler.
|
|
||||||
type TelemetryHandler struct {
|
|
||||||
*ErrorHandler
|
|
||||||
playbookRunService app.PlaybookRunService
|
|
||||||
playbookRunTelemetry app.PlaybookRunTelemetry
|
|
||||||
playbookService app.PlaybookService
|
|
||||||
permissions *app.PermissionsService
|
|
||||||
playbookTelemetry app.PlaybookTelemetry
|
|
||||||
genericTelemetry app.GenericTelemetry
|
|
||||||
botTelemetry bot.Telemetry
|
|
||||||
api playbooks.ServicesAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTelemetryHandler Creates a new Plugin API handler.
|
|
||||||
func NewTelemetryHandler(
|
|
||||||
router *mux.Router,
|
|
||||||
playbookRunService app.PlaybookRunService,
|
|
||||||
api playbooks.ServicesAPI,
|
|
||||||
playbookRunTelemetry app.PlaybookRunTelemetry,
|
|
||||||
playbookService app.PlaybookService,
|
|
||||||
playbookTelemetry app.PlaybookTelemetry,
|
|
||||||
genericTelemetry app.GenericTelemetry,
|
|
||||||
botTelemetry bot.Telemetry,
|
|
||||||
permissions *app.PermissionsService,
|
|
||||||
) *TelemetryHandler {
|
|
||||||
handler := &TelemetryHandler{
|
|
||||||
ErrorHandler: &ErrorHandler{},
|
|
||||||
playbookRunService: playbookRunService,
|
|
||||||
playbookRunTelemetry: playbookRunTelemetry,
|
|
||||||
playbookService: playbookService,
|
|
||||||
playbookTelemetry: playbookTelemetry,
|
|
||||||
genericTelemetry: genericTelemetry,
|
|
||||||
botTelemetry: botTelemetry,
|
|
||||||
api: api,
|
|
||||||
permissions: permissions,
|
|
||||||
}
|
|
||||||
|
|
||||||
telemetryRouter := router.PathPrefix("/telemetry").Subrouter()
|
|
||||||
telemetryRouter.HandleFunc("", withContext(handler.createEvent)).Methods(http.MethodPost)
|
|
||||||
|
|
||||||
startTrialRouter := telemetryRouter.PathPrefix("/start-trial").Subrouter()
|
|
||||||
startTrialRouter.HandleFunc("", withContext(handler.startTrial)).Methods(http.MethodPost)
|
|
||||||
|
|
||||||
playbookRunTelemetryRouterAuthorized := telemetryRouter.PathPrefix("/run").Subrouter()
|
|
||||||
playbookRunTelemetryRouterAuthorized.Use(handler.checkPlaybookRunViewPermissions)
|
|
||||||
playbookRunTelemetryRouterAuthorized.HandleFunc("/{id:[A-Za-z0-9]+}", withContext(handler.telemetryForPlaybookRun)).Methods(http.MethodPost)
|
|
||||||
|
|
||||||
playbookTelemetryRouterAuthorized := telemetryRouter.PathPrefix("/playbook").Subrouter()
|
|
||||||
playbookTelemetryRouterAuthorized.Use(handler.checkPlaybookViewPermissions)
|
|
||||||
playbookTelemetryRouterAuthorized.HandleFunc("/{id:[A-Za-z0-9]+}", withContext(handler.telemetryForPlaybook)).Methods(http.MethodPost)
|
|
||||||
|
|
||||||
templateRouter := telemetryRouter.PathPrefix("/template").Subrouter()
|
|
||||||
templateRouter.HandleFunc("", withContext(handler.telemetryForTemplate))
|
|
||||||
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventData struct {
|
|
||||||
Name string
|
|
||||||
Type app.TelemetryType
|
|
||||||
Properties map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *TelemetryHandler) createEvent(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
var event EventData
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.Properties == nil {
|
|
||||||
event.Properties = map[string]interface{}{}
|
|
||||||
}
|
|
||||||
event.Properties["UserActualID"] = r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
switch event.Type {
|
|
||||||
case app.TelemetryTypePage:
|
|
||||||
name, err := app.NewTelemetryPage(event.Name)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid page tracking", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.genericTelemetry.Page(*name, event.Properties)
|
|
||||||
case app.TelemetryTypeTrack:
|
|
||||||
name, err := app.NewTelemetryTrack(event.Name)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid event tracking", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.genericTelemetry.Track(*name, event.Properties)
|
|
||||||
default:
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid type to be tracked", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *TelemetryHandler) checkPlaybookRunViewPermissions(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
runID := vars["id"]
|
|
||||||
|
|
||||||
if err := h.permissions.RunView(userID, runID); err != nil {
|
|
||||||
logger := getLogger(r)
|
|
||||||
if errors.Is(err, app.ErrNoPermissions) {
|
|
||||||
h.HandleErrorWithCode(w, logger, http.StatusForbidden, "Not authorized", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.HandleError(w, logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *TelemetryHandler) checkPlaybookViewPermissions(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
playbookID := vars["id"]
|
|
||||||
|
|
||||||
if err := h.permissions.PlaybookView(userID, playbookID); err != nil {
|
|
||||||
logger := getLogger(r)
|
|
||||||
if errors.Is(err, app.ErrNoPermissions) {
|
|
||||||
h.HandleErrorWithCode(w, logger, http.StatusForbidden, "Not authorized", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.HandleError(w, logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type TrackerPayload struct {
|
|
||||||
Action string `json:"action"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// telemetryForPlaybookRun handles the /telemetry/run/{id}?action=the_action endpoint. The frontend
|
|
||||||
// can use this endpoint to track events that occur in the context of a playbook run.
|
|
||||||
func (h *TelemetryHandler) telemetryForPlaybookRun(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["id"]
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
var params TrackerPayload
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.Action == "" {
|
|
||||||
h.HandleError(w, c.logger, errors.New("must provide action"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookRun, err := h.playbookRunService.GetPlaybookRun(id)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.playbookRunTelemetry.FrontendTelemetryForPlaybookRun(playbookRun, userID, params.Action)
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *TelemetryHandler) startTrial(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
var params TrackerPayload
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.botTelemetry.StartTrial(userID, params.Action)
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// telemetryForPlaybook handles the /telemetry/playbook/{id}?action=the_action endpoint. The frontend
|
|
||||||
// can use this endpoint to track events that occur in the context of a playbook.
|
|
||||||
func (h *TelemetryHandler) telemetryForPlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["id"]
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
var params TrackerPayload
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.Action == "" {
|
|
||||||
h.HandleError(w, c.logger, errors.New("must provide action"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
playbook, err := h.playbookService.Get(id)
|
|
||||||
if err != nil {
|
|
||||||
h.HandleError(w, c.logger, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.playbookTelemetry.FrontendTelemetryForPlaybook(playbook, userID, params.Action)
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *TelemetryHandler) telemetryForTemplate(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Header.Get("Mattermost-User-ID")
|
|
||||||
|
|
||||||
var params struct {
|
|
||||||
TemplateName string `json:"template_name"`
|
|
||||||
Action string `json:"action"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
|
||||||
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.TemplateName == "" {
|
|
||||||
h.HandleError(w, c.logger, errors.New("must provide template_name"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if params.Action == "" {
|
|
||||||
h.HandleError(w, c.logger, errors.New("must provide action"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.playbookTelemetry.FrontendTelemetryForPlaybookTemplate(params.TemplateName, userID, params.Action)
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultBaseAPIURL = "plugins/playbooks/api/v0"
|
|
||||||
|
|
||||||
func getAPIBaseURL(api playbooks.ServicesAPI) (string, error) {
|
|
||||||
siteURL := model.ServiceSettingsDefaultSiteURL
|
|
||||||
if api.GetConfig().ServiceSettings.SiteURL != nil {
|
|
||||||
siteURL = *api.GetConfig().ServiceSettings.SiteURL
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedSiteURL, err := url.Parse(siteURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrapf(err, "failed to parse siteURL %s", siteURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.Join(parsedSiteURL.Path, defaultBaseAPIURL), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeAPIURL(api playbooks.ServicesAPI, apiPath string, args ...interface{}) string {
|
|
||||||
apiBaseURL, err := getAPIBaseURL(api)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Error("failed to build api base url")
|
|
||||||
apiBaseURL = defaultBaseAPIURL
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.Join("/", apiBaseURL, fmt.Sprintf(apiPath, args...))
|
|
||||||
}
|
|
||||||
@@ -1,457 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/client"
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestActionCreation(t *testing.T) {
|
|
||||||
e := Setup(t)
|
|
||||||
e.CreateBasic()
|
|
||||||
|
|
||||||
createNewChannel := func(t *testing.T, name string) *model.Channel {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
pubChannel, _, err := e.ServerAdminClient.CreateChannel(context.Background(), &model.Channel{
|
|
||||||
DisplayName: name,
|
|
||||||
Name: name,
|
|
||||||
Type: model.ChannelTypeOpen,
|
|
||||||
TeamId: e.BasicTeam.Id,
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
_, _, err = e.ServerAdminClient.AddChannelMember(context.Background(), pubChannel.Id, e.RegularUser.Id)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
return pubChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("create valid action", func(t *testing.T) {
|
|
||||||
// Create a brand new channel
|
|
||||||
channel := createNewChannel(t, "create-valid-action")
|
|
||||||
|
|
||||||
// Create a valid action
|
|
||||||
actionID, err := e.PlaybooksClient.Actions.Create(context.Background(), channel.Id, client.ChannelActionCreateOptions{
|
|
||||||
ChannelID: channel.Id,
|
|
||||||
Enabled: true,
|
|
||||||
ActionType: client.ActionTypeWelcomeMessage,
|
|
||||||
TriggerType: client.TriggerTypeNewMemberJoins,
|
|
||||||
Payload: client.WelcomeMessagePayload{
|
|
||||||
Message: "Hello!",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Verify that the API succeeds
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, actionID)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create valid partial action", func(t *testing.T) {
|
|
||||||
// Create a brand new channel
|
|
||||||
channel := createNewChannel(t, "create-valid-partial-action")
|
|
||||||
|
|
||||||
// Create an action with only keywords, but no playbook ID
|
|
||||||
actionID, err := e.PlaybooksClient.Actions.Create(context.Background(), channel.Id, client.ChannelActionCreateOptions{
|
|
||||||
ChannelID: channel.Id,
|
|
||||||
Enabled: true,
|
|
||||||
ActionType: client.ActionTypePromptRunPlaybook,
|
|
||||||
TriggerType: client.TriggerTypeKeywordsPosted,
|
|
||||||
Payload: client.PromptRunPlaybookFromKeywordsPayload{
|
|
||||||
Keywords: []string{"one"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Verify that the API succeeds
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, actionID)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create invalid action - duplicate action and trigger types", func(t *testing.T) {
|
|
||||||
// Create a brand new channel
|
|
||||||
channel := createNewChannel(t, "create-invalid-action-duplicate")
|
|
||||||
|
|
||||||
// Define an action
|
|
||||||
action := client.ChannelActionCreateOptions{
|
|
||||||
ChannelID: channel.Id,
|
|
||||||
Enabled: true,
|
|
||||||
ActionType: client.ActionTypeCategorizeChannel,
|
|
||||||
TriggerType: client.TriggerTypeNewMemberJoins,
|
|
||||||
Payload: client.CategorizeChannelPayload{
|
|
||||||
CategoryName: "category",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a valid action
|
|
||||||
actionID, err := e.PlaybooksClient.Actions.Create(context.Background(), channel.Id, action)
|
|
||||||
|
|
||||||
// Verify that the API succeeds
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, actionID)
|
|
||||||
|
|
||||||
// Try to create the same action again
|
|
||||||
_, err = e.PlaybooksClient.Actions.Create(context.Background(), channel.Id, action)
|
|
||||||
|
|
||||||
// Verify that the API fails with a 500 error
|
|
||||||
requireErrorWithStatusCode(t, err, http.StatusInternalServerError)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create invalid action - wrong action type", func(t *testing.T) {
|
|
||||||
// Create a brand new channel
|
|
||||||
channel := createNewChannel(t, "create-invalid-action-wrong-action")
|
|
||||||
|
|
||||||
// Create an action with a wrong action type
|
|
||||||
_, err := e.PlaybooksClient.Actions.Create(context.Background(), channel.Id, client.ChannelActionCreateOptions{
|
|
||||||
ChannelID: channel.Id,
|
|
||||||
Enabled: true,
|
|
||||||
ActionType: "wrong action type",
|
|
||||||
TriggerType: client.TriggerTypeNewMemberJoins,
|
|
||||||
Payload: client.WelcomeMessagePayload{
|
|
||||||
Message: "Hello!",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Verify that the API fails with a 400 error
|
|
||||||
requireErrorWithStatusCode(t, err, http.StatusBadRequest)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create invalid action - wrong trigger type", func(t *testing.T) {
|
|
||||||
// Create a brand new channel
|
|
||||||
channel := createNewChannel(t, "create-invalid-action-wrong-trigger")
|
|
||||||
|
|
||||||
// Create an action with a wrong trigger type
|
|
||||||
_, err := e.PlaybooksClient.Actions.Create(context.Background(), channel.Id, client.ChannelActionCreateOptions{
|
|
||||||
ChannelID: channel.Id,
|
|
||||||
Enabled: true,
|
|
||||||
ActionType: client.ActionTypeWelcomeMessage,
|
|
||||||
TriggerType: "wrong trigger type",
|
|
||||||
Payload: client.WelcomeMessagePayload{
|
|
||||||
Message: "Hello!",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Verify that the API fails with a 400 error
|
|
||||||
requireErrorWithStatusCode(t, err, http.StatusBadRequest)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create action forbidden - not channel admin", func(t *testing.T) {
|
|
||||||
// Create a brand new channel
|
|
||||||
channel := createNewChannel(t, "create-action-forbidden")
|
|
||||||
|
|
||||||
defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions()
|
|
||||||
defer func() {
|
|
||||||
e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Tweak the permissions so that the user is no longer channel admin
|
|
||||||
e.Permissions.RemovePermissionFromRole(model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId)
|
|
||||||
|
|
||||||
// Attempt to create the action without those permissions
|
|
||||||
_, err := e.PlaybooksClient.Actions.Create(context.Background(), channel.Id, client.ChannelActionCreateOptions{
|
|
||||||
ChannelID: channel.Id,
|
|
||||||
Enabled: true,
|
|
||||||
ActionType: client.ActionTypeWelcomeMessage,
|
|
||||||
TriggerType: client.TriggerTypeNewMemberJoins,
|
|
||||||
Payload: client.WelcomeMessagePayload{
|
|
||||||
Message: "Hello!",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Verify that the API fails with a 403 error
|
|
||||||
requireErrorWithStatusCode(t, err, http.StatusForbidden)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create action allowed - not channel admin, but system admin", func(t *testing.T) {
|
|
||||||
// Create a brand new channel
|
|
||||||
channel := createNewChannel(t, "create-action-allowed")
|
|
||||||
|
|
||||||
defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions()
|
|
||||||
defer func() {
|
|
||||||
e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Tweak the permissions so that the user is no longer channel admin
|
|
||||||
e.Permissions.RemovePermissionFromRole(model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId)
|
|
||||||
|
|
||||||
// Attempt to create the action as a sysadmin without being a channel admin
|
|
||||||
actionID, err := e.PlaybooksAdminClient.Actions.Create(context.Background(), channel.Id, client.ChannelActionCreateOptions{
|
|
||||||
ChannelID: channel.Id,
|
|
||||||
Enabled: true,
|
|
||||||
ActionType: client.ActionTypePromptRunPlaybook,
|
|
||||||
TriggerType: client.TriggerTypeKeywordsPosted,
|
|
||||||
Payload: client.PromptRunPlaybookFromKeywordsPayload{
|
|
||||||
Keywords: []string{"one", "two"},
|
|
||||||
PlaybookID: model.NewId(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Verify that the API succeeds
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, actionID)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestActionList(t *testing.T) {
|
|
||||||
e := Setup(t)
|
|
||||||
e.CreateBasic()
|
|
||||||
|
|
||||||
// Create three valid actions
|
|
||||||
|
|
||||||
welcomeActionID, err := e.PlaybooksClient.Actions.Create(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionCreateOptions{
|
|
||||||
ChannelID: e.BasicPublicChannel.Id,
|
|
||||||
Enabled: true,
|
|
||||||
ActionType: client.ActionTypeWelcomeMessage,
|
|
||||||
TriggerType: client.TriggerTypeNewMemberJoins,
|
|
||||||
Payload: client.WelcomeMessagePayload{
|
|
||||||
Message: "msg",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
categorizeActionID, err := e.PlaybooksClient.Actions.Create(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionCreateOptions{
|
|
||||||
ChannelID: e.BasicPublicChannel.Id,
|
|
||||||
Enabled: true,
|
|
||||||
ActionType: client.ActionTypeCategorizeChannel,
|
|
||||||
TriggerType: client.TriggerTypeNewMemberJoins,
|
|
||||||
Payload: client.CategorizeChannelPayload{
|
|
||||||
CategoryName: "category",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
playbookID := model.NewId()
|
|
||||||
promptActionID, err := e.PlaybooksClient.Actions.Create(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionCreateOptions{
|
|
||||||
ChannelID: e.BasicPublicChannel.Id,
|
|
||||||
Enabled: true,
|
|
||||||
ActionType: client.ActionTypePromptRunPlaybook,
|
|
||||||
TriggerType: client.TriggerTypeKeywordsPosted,
|
|
||||||
Payload: client.PromptRunPlaybookFromKeywordsPayload{
|
|
||||||
Keywords: []string{"one", "two"},
|
|
||||||
PlaybookID: playbookID,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
t.Run("view list allowed", func(t *testing.T) {
|
|
||||||
// List the actions with the default options
|
|
||||||
actions, err := e.PlaybooksClient.Actions.List(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionListOptions{})
|
|
||||||
|
|
||||||
// Verify that the API succeeds and that it returns the correct number of actions
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Len(t, actions, 3)
|
|
||||||
|
|
||||||
// Verify that the returned actions contain the correct payloads
|
|
||||||
for _, action := range actions {
|
|
||||||
switch action.ID {
|
|
||||||
case welcomeActionID:
|
|
||||||
var payload client.WelcomeMessagePayload
|
|
||||||
err = mapstructure.Decode(action.Payload, &payload)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "msg", payload.Message)
|
|
||||||
|
|
||||||
case categorizeActionID:
|
|
||||||
var payload client.CategorizeChannelPayload
|
|
||||||
err = mapstructure.Decode(action.Payload, &payload)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "category", payload.CategoryName)
|
|
||||||
|
|
||||||
case promptActionID:
|
|
||||||
var payload client.PromptRunPlaybookFromKeywordsPayload
|
|
||||||
err = mapstructure.Decode(action.Payload, &payload)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.EqualValues(t, []string{"one", "two"}, payload.Keywords)
|
|
||||||
assert.Equal(t, playbookID, payload.PlaybookID)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("view list forbidden", func(t *testing.T) {
|
|
||||||
defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions()
|
|
||||||
defer func() {
|
|
||||||
e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Tweak the permissions so that the user is no longer channel admin
|
|
||||||
e.Permissions.RemovePermissionFromRole(model.PermissionReadChannel.Id, model.ChannelUserRoleId)
|
|
||||||
|
|
||||||
// Attempt to list the actions
|
|
||||||
_, err := e.PlaybooksClient.Actions.List(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionListOptions{})
|
|
||||||
|
|
||||||
// Verify that the API fails with a 403 error
|
|
||||||
requireErrorWithStatusCode(t, err, http.StatusForbidden)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestActionUpdate(t *testing.T) {
|
|
||||||
e := Setup(t)
|
|
||||||
e.CreateBasic()
|
|
||||||
|
|
||||||
// Create a valid action
|
|
||||||
action := client.GenericChannelAction{
|
|
||||||
GenericChannelActionWithoutPayload: client.GenericChannelActionWithoutPayload{
|
|
||||||
ChannelID: e.BasicPublicChannel.Id,
|
|
||||||
Enabled: true,
|
|
||||||
ActionType: client.ActionTypeWelcomeMessage,
|
|
||||||
TriggerType: client.TriggerTypeNewMemberJoins,
|
|
||||||
},
|
|
||||||
Payload: client.WelcomeMessagePayload{
|
|
||||||
Message: "msg",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := e.PlaybooksClient.Actions.Create(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionCreateOptions{
|
|
||||||
ChannelID: e.BasicPublicChannel.Id,
|
|
||||||
Enabled: action.Enabled,
|
|
||||||
ActionType: action.ActionType,
|
|
||||||
TriggerType: action.TriggerType,
|
|
||||||
Payload: action.Payload,
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, id)
|
|
||||||
|
|
||||||
action.ID = id
|
|
||||||
|
|
||||||
t.Run("valid update", func(t *testing.T) {
|
|
||||||
// Make a valid modification
|
|
||||||
action.Enabled = false
|
|
||||||
|
|
||||||
// Make the Update request
|
|
||||||
err := e.PlaybooksClient.Actions.Update(context.Background(), action)
|
|
||||||
|
|
||||||
// Verify that the API succeeds
|
|
||||||
assert.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid update - remove keywords from action", func(t *testing.T) {
|
|
||||||
payload := client.PromptRunPlaybookFromKeywordsPayload{
|
|
||||||
Keywords: []string{"one"},
|
|
||||||
PlaybookID: e.BasicPlaybook.ID,
|
|
||||||
}
|
|
||||||
|
|
||||||
newAction := client.GenericChannelAction{
|
|
||||||
GenericChannelActionWithoutPayload: client.GenericChannelActionWithoutPayload{
|
|
||||||
ChannelID: e.BasicPublicChannel.Id,
|
|
||||||
Enabled: true,
|
|
||||||
ActionType: client.ActionTypePromptRunPlaybook,
|
|
||||||
TriggerType: client.TriggerTypeKeywordsPosted,
|
|
||||||
},
|
|
||||||
Payload: payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an action with keywords and playbook ID
|
|
||||||
actionID, err := e.PlaybooksClient.Actions.Create(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionCreateOptions{
|
|
||||||
ChannelID: newAction.ChannelID,
|
|
||||||
Enabled: newAction.Enabled,
|
|
||||||
ActionType: newAction.ActionType,
|
|
||||||
TriggerType: newAction.TriggerType,
|
|
||||||
Payload: newAction.Payload,
|
|
||||||
})
|
|
||||||
newAction.ID = actionID
|
|
||||||
|
|
||||||
// Verify that the API succeeds
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, actionID)
|
|
||||||
|
|
||||||
// Retrieve the newly created action and decode its payload
|
|
||||||
actions, err := e.PlaybooksClient.Actions.List(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionListOptions{
|
|
||||||
TriggerType: client.TriggerTypeKeywordsPosted,
|
|
||||||
ActionType: client.ActionTypePromptRunPlaybook,
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Len(t, actions, 1)
|
|
||||||
fetchedAction := actions[0]
|
|
||||||
var fetchedPayload client.PromptRunPlaybookFromKeywordsPayload
|
|
||||||
err = mapstructure.Decode(fetchedAction.Payload, &fetchedPayload)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify that the payload of the created action has one keyword
|
|
||||||
assert.Len(t, fetchedPayload.Keywords, 1)
|
|
||||||
|
|
||||||
// Remove the keywords from the payload in the action
|
|
||||||
payload.Keywords = []string{}
|
|
||||||
newAction.Payload = payload
|
|
||||||
|
|
||||||
// Make the Update request with the new action
|
|
||||||
err = e.PlaybooksClient.Actions.Update(context.Background(), newAction)
|
|
||||||
|
|
||||||
// Verify that the API succeeds
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Retrieve the updated action and decode its payload
|
|
||||||
updatedActions, err := e.PlaybooksClient.Actions.List(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionListOptions{
|
|
||||||
TriggerType: client.TriggerTypeKeywordsPosted,
|
|
||||||
ActionType: client.ActionTypePromptRunPlaybook,
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Len(t, updatedActions, 1)
|
|
||||||
updatedAction := updatedActions[0]
|
|
||||||
var updatedPayload client.PromptRunPlaybookFromKeywordsPayload
|
|
||||||
err = mapstructure.Decode(updatedAction.Payload, &updatedPayload)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify that the payload of the updated action has no keywords
|
|
||||||
assert.Len(t, updatedPayload.Keywords, 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid update - wrong action type", func(t *testing.T) {
|
|
||||||
// Make an invalid modification
|
|
||||||
action.ActionType = "wrong"
|
|
||||||
|
|
||||||
// Make the Update request
|
|
||||||
err := e.PlaybooksClient.Actions.Update(context.Background(), action)
|
|
||||||
|
|
||||||
// Verify that the API fails with a 400 error
|
|
||||||
requireErrorWithStatusCode(t, err, http.StatusBadRequest)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid update - wrong trigger type", func(t *testing.T) {
|
|
||||||
// Make an invalid modification
|
|
||||||
action.TriggerType = "wrong"
|
|
||||||
|
|
||||||
// Make the Update request
|
|
||||||
err := e.PlaybooksClient.Actions.Update(context.Background(), action)
|
|
||||||
|
|
||||||
// Verify that the API fails with a 400 error
|
|
||||||
requireErrorWithStatusCode(t, err, http.StatusBadRequest)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid update - wrong payload type", func(t *testing.T) {
|
|
||||||
// Make an invalid modification
|
|
||||||
action.Payload = client.WelcomeMessagePayload{Message: ""}
|
|
||||||
|
|
||||||
// Make the Update request
|
|
||||||
err := e.PlaybooksClient.Actions.Update(context.Background(), action)
|
|
||||||
|
|
||||||
// Verify that the API fails with a 400 error
|
|
||||||
requireErrorWithStatusCode(t, err, http.StatusBadRequest)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update action forbidden - not channel admin", func(t *testing.T) {
|
|
||||||
defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions()
|
|
||||||
defer func() {
|
|
||||||
e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Tweak the permissions so that the user is no longer channel admin
|
|
||||||
e.Permissions.RemovePermissionFromRole(model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId)
|
|
||||||
|
|
||||||
// Make a valid modification
|
|
||||||
action.Enabled = false
|
|
||||||
|
|
||||||
// Make the Update request
|
|
||||||
err := e.PlaybooksClient.Actions.Update(context.Background(), action)
|
|
||||||
|
|
||||||
// Verify that the API fails with a 403 error
|
|
||||||
requireErrorWithStatusCode(t, err, http.StatusForbidden)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTrialLicences(t *testing.T) {
|
|
||||||
// This test is flaky due to upstream connectivity issues.
|
|
||||||
t.Skip()
|
|
||||||
|
|
||||||
e := Setup(t)
|
|
||||||
e.CreateBasic()
|
|
||||||
|
|
||||||
t.Run("request trial license without permissions", func(t *testing.T) {
|
|
||||||
dialogRequest := model.PostActionIntegrationRequest{
|
|
||||||
UserId: e.RegularUser.Id,
|
|
||||||
PostId: e.BasicPublicChannelPost.Id,
|
|
||||||
Context: map[string]interface{}{
|
|
||||||
"users": 10,
|
|
||||||
"termsAccepted": true,
|
|
||||||
"receiveEmailsAccepted": true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dialogRequestBytes, _ := json.Marshal(dialogRequest)
|
|
||||||
resp, err := e.ServerClient.DoAPIRequestBytes(context.Background(), "POST", e.ServerClient.URL+"/plugins/"+"playbooks"+"/api/v0/bot/notify-admins/button-start-trial", dialogRequestBytes, "")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("request trial license with permissions", func(t *testing.T) {
|
|
||||||
dialogRequest := model.PostActionIntegrationRequest{
|
|
||||||
UserId: e.AdminUser.Id,
|
|
||||||
PostId: e.BasicPublicChannelPost.Id,
|
|
||||||
Context: map[string]interface{}{
|
|
||||||
"users": 10,
|
|
||||||
"termsAccepted": true,
|
|
||||||
"receiveEmailsAccepted": true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dialogRequestBytes, _ := json.Marshal(dialogRequest)
|
|
||||||
resp, err := e.ServerAdminClient.DoAPIRequestBytes(context.Background(), "POST", e.ServerClient.URL+"/plugins/"+"playbooks"+"/api/v0/bot/notify-admins/button-start-trial", dialogRequestBytes, "")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAPI(t *testing.T) {
|
|
||||||
e := Setup(t)
|
|
||||||
e.CreateClients()
|
|
||||||
|
|
||||||
t.Run("404", func(t *testing.T) {
|
|
||||||
resp, err := e.ServerClient.DoAPIRequestBytes(context.Background(), "POST", e.ServerClient.URL+"/plugins/"+"playbooks"+"/api/v0/nothing", nil, "")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,691 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/graph-gophers/graphql-go"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/client"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/api"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/app"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGraphQLPlaybooks(t *testing.T) {
|
|
||||||
e := Setup(t)
|
|
||||||
e.CreateBasic()
|
|
||||||
|
|
||||||
t.Run("basic get", func(t *testing.T) {
|
|
||||||
var pbResultTest struct {
|
|
||||||
Data struct {
|
|
||||||
Playbook struct {
|
|
||||||
ID string
|
|
||||||
Title string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
testPlaybookQuery := `
|
|
||||||
query Playbook($id: String!) {
|
|
||||||
playbook(id: $id) {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
err := e.PlaybooksAdminClient.DoGraphql(context.Background(), &client.GraphQLInput{
|
|
||||||
Query: testPlaybookQuery,
|
|
||||||
OperationName: "Playbook",
|
|
||||||
Variables: map[string]interface{}{"id": e.BasicPlaybook.ID},
|
|
||||||
}, &pbResultTest)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, e.BasicPlaybook.ID, pbResultTest.Data.Playbook.ID)
|
|
||||||
assert.Equal(t, e.BasicPlaybook.Title, pbResultTest.Data.Playbook.Title)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("list", func(t *testing.T) {
|
|
||||||
var pbResultTest struct {
|
|
||||||
Data struct {
|
|
||||||
Playbooks []struct {
|
|
||||||
ID string
|
|
||||||
Title string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
testPlaybookQuery := `
|
|
||||||
query Playbooks {
|
|
||||||
playbooks {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
err := e.PlaybooksAdminClient.DoGraphql(context.Background(), &client.GraphQLInput{
|
|
||||||
Query: testPlaybookQuery,
|
|
||||||
OperationName: "Playbooks",
|
|
||||||
}, &pbResultTest)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Len(t, pbResultTest.Data.Playbooks, 3)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("playbook mutate", func(t *testing.T) {
|
|
||||||
newUpdatedTitle := "graphqlmutatetitle"
|
|
||||||
|
|
||||||
err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{"title": newUpdatedTitle})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
updatedPlaybook, err := e.PlaybooksAdminClient.Playbooks.Get(context.Background(), e.BasicPlaybook.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, newUpdatedTitle, updatedPlaybook.Title)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update playbook no permissions to broadcast", func(t *testing.T) {
|
|
||||||
err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{"broadcastChannelIDs": []string{e.BasicPrivateChannel.Id}})
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update playbook without modifying broadcast channel ids without permission. should succeed because no modification.", func(t *testing.T) {
|
|
||||||
e.BasicPlaybook.BroadcastChannelIDs = []string{e.BasicPrivateChannel.Id}
|
|
||||||
err := e.PlaybooksAdminClient.Playbooks.Update(context.Background(), *e.BasicPlaybook)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{"description": "unrelatedupdate"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update playbook with too many webhoooks", func(t *testing.T) {
|
|
||||||
urls := []string{}
|
|
||||||
for i := 0; i < 65; i++ {
|
|
||||||
urls = append(urls, "http://localhost/"+strconv.Itoa(i))
|
|
||||||
}
|
|
||||||
err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{
|
|
||||||
"webhookOnCreationEnabled": true,
|
|
||||||
"webhookOnCreationURLs": urls,
|
|
||||||
})
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("change default owner", func(t *testing.T) {
|
|
||||||
err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{
|
|
||||||
"defaultOwnerID": e.RegularUser.Id,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{
|
|
||||||
"defaultOwnerID": e.RegularUserNotInTeam.Id,
|
|
||||||
})
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
t.Run("checklist with preset values that need to be cleared", func(t *testing.T) {
|
|
||||||
items := []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"title": "title1",
|
|
||||||
"description": "description1",
|
|
||||||
"assigneeID": "",
|
|
||||||
"assigneeModified": 101,
|
|
||||||
"state": "Closed",
|
|
||||||
"stateModified": 102,
|
|
||||||
"command": "",
|
|
||||||
"commandLastRun": 103,
|
|
||||||
"lastSkipped": 104,
|
|
||||||
"dueDate": 100,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{
|
|
||||||
"checklists": map[string]interface{}{
|
|
||||||
"title": "A",
|
|
||||||
"items": items,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
actual := []client.Checklist{
|
|
||||||
{
|
|
||||||
Title: "A",
|
|
||||||
Items: []client.ChecklistItem{
|
|
||||||
{
|
|
||||||
Title: "title1",
|
|
||||||
Description: "description1",
|
|
||||||
AssigneeID: "",
|
|
||||||
AssigneeModified: 0,
|
|
||||||
State: "",
|
|
||||||
StateModified: 0,
|
|
||||||
Command: "",
|
|
||||||
CommandLastRun: 0,
|
|
||||||
LastSkipped: 0,
|
|
||||||
DueDate: 100,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
updatedPlaybook, err := e.PlaybooksAdminClient.Playbooks.Get(context.Background(), e.BasicPlaybook.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, updatedPlaybook.Checklists, actual)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update playbook with pre-assigned task, valid invite user list, and invitations enabled", func(t *testing.T) {
|
|
||||||
items := []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"title": "title1",
|
|
||||||
"description": "description1",
|
|
||||||
"assigneeID": e.RegularUser.Id,
|
|
||||||
"assigneeModified": 0,
|
|
||||||
"state": "",
|
|
||||||
"stateModified": 0,
|
|
||||||
"command": "",
|
|
||||||
"commandLastRun": 0,
|
|
||||||
"lastSkipped": 0,
|
|
||||||
"dueDate": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{
|
|
||||||
"checklists": map[string]interface{}{
|
|
||||||
"title": "A",
|
|
||||||
"items": items,
|
|
||||||
},
|
|
||||||
"invitedUserIDs": []string{e.RegularUser.Id},
|
|
||||||
"inviteUsersEnabled": true,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
func TestGraphQLUpdatePlaybookFails(t *testing.T) {
|
|
||||||
e := Setup(t)
|
|
||||||
e.CreateBasic()
|
|
||||||
|
|
||||||
t.Run("update playbook fails because size constraints.", func(t *testing.T) {
|
|
||||||
e.BasicPlaybook.BroadcastChannelIDs = []string{e.BasicPrivateChannel.Id}
|
|
||||||
|
|
||||||
err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{
|
|
||||||
"checklists": []api.UpdateChecklist{
|
|
||||||
{
|
|
||||||
Title: strings.Repeat("A", (256*1024)+1),
|
|
||||||
Items: []api.UpdateChecklistItem{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
err = gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{"title": strings.Repeat("A", 1025)})
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
err = gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{"description": strings.Repeat("A", 4097)})
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update playbook with pre-assigned task fails due to disabled invitations", func(t *testing.T) {
|
|
||||||
items := []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"title": "title1",
|
|
||||||
"description": "description1",
|
|
||||||
"assigneeID": e.RegularUser.Id,
|
|
||||||
"assigneeModified": 0,
|
|
||||||
"state": "",
|
|
||||||
"stateModified": 0,
|
|
||||||
"command": "",
|
|
||||||
"commandLastRun": 0,
|
|
||||||
"lastSkipped": 0,
|
|
||||||
"dueDate": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{
|
|
||||||
"checklists": map[string]interface{}{
|
|
||||||
"title": "A",
|
|
||||||
"items": items,
|
|
||||||
},
|
|
||||||
"invitedUserIDs": []string{e.RegularUser.Id},
|
|
||||||
})
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update playbook with pre-assigned task fails due to missing assignee in existing invite user list", func(t *testing.T) {
|
|
||||||
items := []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"title": "title1",
|
|
||||||
"description": "description1",
|
|
||||||
"assigneeID": e.RegularUser.Id,
|
|
||||||
"assigneeModified": 0,
|
|
||||||
"state": "",
|
|
||||||
"stateModified": 0,
|
|
||||||
"command": "",
|
|
||||||
"commandLastRun": 0,
|
|
||||||
"lastSkipped": 0,
|
|
||||||
"dueDate": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{
|
|
||||||
"checklists": map[string]interface{}{
|
|
||||||
"title": "A",
|
|
||||||
"items": items,
|
|
||||||
},
|
|
||||||
"inviteUsersEnabled": true,
|
|
||||||
})
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update playbook with pre-assigned task fails due to assignee missing in new invite user list", func(t *testing.T) {
|
|
||||||
items := []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"title": "title1",
|
|
||||||
"description": "description1",
|
|
||||||
"assigneeID": e.RegularUser.Id,
|
|
||||||
"assigneeModified": 0,
|
|
||||||
"state": "",
|
|
||||||
"stateModified": 0,
|
|
||||||
"command": "",
|
|
||||||
"commandLastRun": 0,
|
|
||||||
"lastSkipped": 0,
|
|
||||||
"dueDate": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{
|
|
||||||
"checklists": map[string]interface{}{
|
|
||||||
"title": "A",
|
|
||||||
"items": items,
|
|
||||||
},
|
|
||||||
"invitedUserIDs": []string{e.RegularUser2.Id},
|
|
||||||
"inviteUsersEnabled": true,
|
|
||||||
})
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update playbook with invite user list fails due to missing a pre-assignee", func(t *testing.T) {
|
|
||||||
items := []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"title": "title1",
|
|
||||||
"description": "description1",
|
|
||||||
"assigneeID": e.RegularUser.Id,
|
|
||||||
"assigneeModified": 0,
|
|
||||||
"state": "",
|
|
||||||
"stateModified": 0,
|
|
||||||
"command": "",
|
|
||||||
"commandLastRun": 0,
|
|
||||||
"lastSkipped": 0,
|
|
||||||
"dueDate": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{
|
|
||||||
"checklists": map[string]interface{}{
|
|
||||||
"title": "A",
|
|
||||||
"items": items,
|
|
||||||
},
|
|
||||||
"invitedUserIDs": []string{e.RegularUser.Id},
|
|
||||||
"inviteUsersEnabled": true,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{
|
|
||||||
"invitedUserIDs": []string{e.RegularUser2.Id},
|
|
||||||
})
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update playbook fails if invitations are getting disabled but there are pre-assigned users", func(t *testing.T) {
|
|
||||||
items := []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"title": "title1",
|
|
||||||
"description": "description1",
|
|
||||||
"assigneeID": e.RegularUser.Id,
|
|
||||||
"assigneeModified": 0,
|
|
||||||
"state": "",
|
|
||||||
"stateModified": 0,
|
|
||||||
"command": "",
|
|
||||||
"commandLastRun": 0,
|
|
||||||
"lastSkipped": 0,
|
|
||||||
"dueDate": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{
|
|
||||||
"checklists": map[string]interface{}{
|
|
||||||
"title": "A",
|
|
||||||
"items": items,
|
|
||||||
},
|
|
||||||
"invitedUserIDs": []string{e.RegularUser.Id},
|
|
||||||
"inviteUsersEnabled": true,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{
|
|
||||||
"inviteUsersEnabled": false,
|
|
||||||
})
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdatePlaybookFavorite(t *testing.T) {
|
|
||||||
e := Setup(t)
|
|
||||||
e.CreateBasic()
|
|
||||||
|
|
||||||
t.Run("favorite", func(t *testing.T) {
|
|
||||||
isFavorite, err := getPlaybookFavorite(e.PlaybooksClient, e.BasicPlaybook.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.False(t, isFavorite)
|
|
||||||
|
|
||||||
response, err := updatePlaybookFavorite(e.PlaybooksClient, e.BasicPlaybook.ID, true)
|
|
||||||
require.Empty(t, response.Errors)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
isFavorite, err = getPlaybookFavorite(e.PlaybooksClient, e.BasicPlaybook.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.True(t, isFavorite)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("unfavorite", func(t *testing.T) {
|
|
||||||
response, err := updatePlaybookFavorite(e.PlaybooksClient, e.BasicPlaybook.ID, false)
|
|
||||||
require.Empty(t, response.Errors)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
isFavorite, err := getPlaybookFavorite(e.PlaybooksClient, e.BasicPlaybook.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.False(t, isFavorite)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("favorite playbook with read access", func(t *testing.T) {
|
|
||||||
response, err := updatePlaybookFavorite(e.PlaybooksClient2, e.BasicPlaybook.ID, true)
|
|
||||||
require.Empty(t, response.Errors)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
isFavorite, err := getPlaybookFavorite(e.PlaybooksClient2, e.BasicPlaybook.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.True(t, isFavorite)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("favorite private playbook no access", func(t *testing.T) {
|
|
||||||
response, _ := updatePlaybookFavorite(e.PlaybooksClient, e.PrivatePlaybookNoMembers.ID, false)
|
|
||||||
require.NotEmpty(t, response.Errors)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func updatePlaybookFavorite(c *client.Client, playbookID string, favorite bool) (graphql.Response, error) {
|
|
||||||
mutation := `mutation UpdatePlaybookFavorite($id: String!, $favorite: Boolean!) {
|
|
||||||
updatePlaybookFavorite(id: $id, favorite: $favorite)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
var response graphql.Response
|
|
||||||
err := c.DoGraphql(context.Background(), &client.GraphQLInput{
|
|
||||||
Query: mutation,
|
|
||||||
OperationName: "UpdatePlaybookFavorite",
|
|
||||||
Variables: map[string]interface{}{
|
|
||||||
"id": playbookID,
|
|
||||||
"favorite": favorite,
|
|
||||||
},
|
|
||||||
}, &response)
|
|
||||||
|
|
||||||
return response, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPlaybookFavorite(c *client.Client, playbookID string) (bool, error) {
|
|
||||||
query := `
|
|
||||||
query GetPlaybookFavorite($id: String!) {
|
|
||||||
playbook(id: $id) {
|
|
||||||
isFavorite
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
var response graphql.Response
|
|
||||||
err := c.DoGraphql(context.Background(), &client.GraphQLInput{
|
|
||||||
Query: query,
|
|
||||||
OperationName: "GetPlaybookFavorite",
|
|
||||||
Variables: map[string]interface{}{
|
|
||||||
"id": playbookID,
|
|
||||||
},
|
|
||||||
}, &response)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if len(response.Errors) > 0 {
|
|
||||||
return false, fmt.Errorf("error from query %v", response.Errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
favoriteResponse := struct {
|
|
||||||
Playbook struct {
|
|
||||||
IsFavorite bool `json:"isFavorite"`
|
|
||||||
} `json:"playbook"`
|
|
||||||
}{}
|
|
||||||
err = json.Unmarshal(response.Data, &favoriteResponse)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return favoriteResponse.Playbook.IsFavorite, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func gqlTestPlaybookUpdate(e *TestEnvironment, t *testing.T, playbookID string, updates map[string]interface{}) error {
|
|
||||||
testPlaybookMutateQuery := `
|
|
||||||
mutation UpdatePlaybook($id: String!, $updates: PlaybookUpdates!) {
|
|
||||||
updatePlaybook(id: $id, updates: $updates)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
var response graphql.Response
|
|
||||||
err := e.PlaybooksClient.DoGraphql(context.Background(), &client.GraphQLInput{
|
|
||||||
Query: testPlaybookMutateQuery,
|
|
||||||
OperationName: "UpdatePlaybook",
|
|
||||||
Variables: map[string]interface{}{"id": playbookID, "updates": updates},
|
|
||||||
}, &response)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "gqlTestPlaybookUpdate graphql failure")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(response.Errors) != 0 {
|
|
||||||
return errors.Errorf("gqlTestPlaybookUpdate graphql failure %+v", response.Errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGraphQLPlaybooksMetrics(t *testing.T) {
|
|
||||||
e := Setup(t)
|
|
||||||
e.CreateBasic()
|
|
||||||
|
|
||||||
t.Run("metrics get", func(t *testing.T) {
|
|
||||||
var pbResultTest struct {
|
|
||||||
Data struct {
|
|
||||||
Playbook struct {
|
|
||||||
ID string
|
|
||||||
Title string
|
|
||||||
Metrics []client.PlaybookMetricConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
testPlaybookQuery :=
|
|
||||||
`
|
|
||||||
query Playbook($id: String!) {
|
|
||||||
playbook(id: $id) {
|
|
||||||
id
|
|
||||||
metrics {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
description
|
|
||||||
type
|
|
||||||
target
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
err := e.PlaybooksAdminClient.DoGraphql(context.Background(), &client.GraphQLInput{
|
|
||||||
Query: testPlaybookQuery,
|
|
||||||
OperationName: "Playbook",
|
|
||||||
Variables: map[string]interface{}{"id": e.BasicPlaybook.ID},
|
|
||||||
}, &pbResultTest)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Len(t, pbResultTest.Data.Playbook.Metrics, len(e.BasicPlaybook.Metrics))
|
|
||||||
require.Equal(t, e.BasicPlaybook.Metrics[0].Title, pbResultTest.Data.Playbook.Metrics[0].Title)
|
|
||||||
require.Equal(t, e.BasicPlaybook.Metrics[0].Type, pbResultTest.Data.Playbook.Metrics[0].Type)
|
|
||||||
require.Equal(t, e.BasicPlaybook.Metrics[0].Target, pbResultTest.Data.Playbook.Metrics[0].Target)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("add metric", func(t *testing.T) {
|
|
||||||
testAddMetricQuery := `
|
|
||||||
mutation AddMetric($playbookID: String!, $title: String!, $description: String!, $type: String!, $target: Int) {
|
|
||||||
addMetric(playbookID: $playbookID, title: $title, description: $description, type: $type, target: $target)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
var response graphql.Response
|
|
||||||
err := e.PlaybooksClient.DoGraphql(context.Background(), &client.GraphQLInput{
|
|
||||||
Query: testAddMetricQuery,
|
|
||||||
OperationName: "AddMetric",
|
|
||||||
Variables: map[string]interface{}{
|
|
||||||
"playbookID": e.BasicPlaybook.ID,
|
|
||||||
"title": "New Metric",
|
|
||||||
"description": "the description",
|
|
||||||
"type": app.MetricTypeDuration,
|
|
||||||
},
|
|
||||||
}, &response)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, response.Errors)
|
|
||||||
|
|
||||||
updatedPlaybook, err := e.PlaybooksAdminClient.Playbooks.Get(context.Background(), e.BasicPlaybook.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Len(t, updatedPlaybook.Metrics, 2)
|
|
||||||
assert.Equal(t, updatedPlaybook.Metrics[1].Title, "New Metric")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update metric", func(t *testing.T) {
|
|
||||||
testUpdateMetricQuery := `
|
|
||||||
mutation UpdateMetric($id: String!, $title: String, $description: String, $target: Int) {
|
|
||||||
updateMetric(id: $id, title: $title, description: $description, target: $target)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
var response graphql.Response
|
|
||||||
err := e.PlaybooksClient.DoGraphql(context.Background(), &client.GraphQLInput{
|
|
||||||
Query: testUpdateMetricQuery,
|
|
||||||
OperationName: "UpdateMetric",
|
|
||||||
Variables: map[string]interface{}{
|
|
||||||
"id": e.BasicPlaybook.Metrics[0].ID,
|
|
||||||
"title": "Updated Title",
|
|
||||||
},
|
|
||||||
}, &response)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, response.Errors)
|
|
||||||
|
|
||||||
updatedPlaybook, err := e.PlaybooksAdminClient.Playbooks.Get(context.Background(), e.BasicPlaybook.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Len(t, updatedPlaybook.Metrics, 2)
|
|
||||||
assert.Equal(t, "Updated Title", updatedPlaybook.Metrics[0].Title)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("delete metric", func(t *testing.T) {
|
|
||||||
testDeleteMetricQuery := `
|
|
||||||
mutation DeleteMetric($id: String!) {
|
|
||||||
deleteMetric(id: $id)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
var response graphql.Response
|
|
||||||
err := e.PlaybooksClient.DoGraphql(context.Background(), &client.GraphQLInput{
|
|
||||||
Query: testDeleteMetricQuery,
|
|
||||||
OperationName: "DeleteMetric",
|
|
||||||
Variables: map[string]interface{}{
|
|
||||||
"id": e.BasicPlaybook.Metrics[0].ID,
|
|
||||||
},
|
|
||||||
}, &response)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, response.Errors)
|
|
||||||
|
|
||||||
updatedPlaybook, err := e.PlaybooksAdminClient.Playbooks.Get(context.Background(), e.BasicPlaybook.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Len(t, updatedPlaybook.Metrics, 1)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func gqlTestPlaybookUpdateGuest(e *TestEnvironment, t *testing.T, playbookID string, updates map[string]interface{}) error {
|
|
||||||
testPlaybookMutateQuery := `
|
|
||||||
mutation UpdatePlaybook($id: String!, $updates: PlaybookUpdates!) {
|
|
||||||
updatePlaybook(id: $id, updates: $updates)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
var response graphql.Response
|
|
||||||
err := e.PlaybooksClientGuest.DoGraphql(context.Background(), &client.GraphQLInput{
|
|
||||||
Query: testPlaybookMutateQuery,
|
|
||||||
OperationName: "UpdatePlaybook",
|
|
||||||
Variables: map[string]interface{}{"id": playbookID, "updates": updates},
|
|
||||||
}, &response)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "gqlTestPlaybookUpdate graphql failure")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(response.Errors) != 0 {
|
|
||||||
return errors.Errorf("gqlTestPlaybookUpdate graphql failure %+v", response.Errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGraphQLPlaybooksGuests(t *testing.T) {
|
|
||||||
e := Setup(t)
|
|
||||||
e.SetE20Licence()
|
|
||||||
e.CreateBasic()
|
|
||||||
e.CreateGuest()
|
|
||||||
|
|
||||||
t.Run("update playbook guest not member", func(t *testing.T) {
|
|
||||||
err := gqlTestPlaybookUpdateGuest(e, t, e.BasicPlaybook.ID, map[string]interface{}{"title": "mutated"})
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("basic get guest not member", func(t *testing.T) {
|
|
||||||
testPlaybookQuery := `
|
|
||||||
query Playbook($id: String!) {
|
|
||||||
playbook(id: $id) {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
var response graphql.Response
|
|
||||||
err := e.PlaybooksClientGuest.DoGraphql(context.Background(), &client.GraphQLInput{
|
|
||||||
Query: testPlaybookQuery,
|
|
||||||
OperationName: "Playbook",
|
|
||||||
Variables: map[string]interface{}{"id": e.BasicPlaybook.ID},
|
|
||||||
}, &response)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotZero(t, len(response.Errors))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("list guest", func(t *testing.T) {
|
|
||||||
var pbResultTest struct {
|
|
||||||
Data struct {
|
|
||||||
Playbooks []struct {
|
|
||||||
ID string
|
|
||||||
Title string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
testPlaybookQuery := `
|
|
||||||
query Playbooks {
|
|
||||||
playbooks {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
err := e.PlaybooksClientGuest.DoGraphql(context.Background(), &client.GraphQLInput{
|
|
||||||
Query: testPlaybookQuery,
|
|
||||||
OperationName: "Playbooks",
|
|
||||||
}, &pbResultTest)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Len(t, pbResultTest.Data.Playbooks, 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,37 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/client"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSettings(t *testing.T) {
|
|
||||||
e := Setup(t)
|
|
||||||
e.CreateBasic()
|
|
||||||
|
|
||||||
t.Run("get settings", func(t *testing.T) {
|
|
||||||
t.Run("unauthenticated", func(t *testing.T) {
|
|
||||||
settings, err := e.UnauthenticatedPlaybooksClient.Settings.Get(context.Background())
|
|
||||||
assert.Nil(t, settings)
|
|
||||||
requireErrorWithStatusCode(t, err, http.StatusUnauthorized)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("get some settings", func(t *testing.T) {
|
|
||||||
defaultSettings := &client.GlobalSettings{
|
|
||||||
EnableExperimentalFeatures: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
settings, err := e.PlaybooksClient.Settings.Get(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, defaultSettings, settings)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/client"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"gopkg.in/guregu/null.v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetSiteStats(t *testing.T) {
|
|
||||||
e := Setup(t)
|
|
||||||
e.CreateBasic()
|
|
||||||
|
|
||||||
t.Run("get sites stats", func(t *testing.T) {
|
|
||||||
t.Run("unauthenticated", func(t *testing.T) {
|
|
||||||
stats, err := e.UnauthenticatedPlaybooksClient.Stats.GetSiteStats(context.Background())
|
|
||||||
assert.Nil(t, stats)
|
|
||||||
requireErrorWithStatusCode(t, err, http.StatusUnauthorized)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("get stats for basic server", func(t *testing.T) {
|
|
||||||
stats, err := e.PlaybooksAdminClient.Stats.GetSiteStats(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, stats)
|
|
||||||
assert.Equal(t, 4, stats.TotalPlaybooks)
|
|
||||||
assert.Equal(t, 1, stats.TotalPlaybookRuns)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("add extra playbooks/runs and get stats again", func(t *testing.T) {
|
|
||||||
e.CreateBasicPlaybook()
|
|
||||||
e.CreateBasicRun()
|
|
||||||
e.CreateBasicRun()
|
|
||||||
|
|
||||||
stats, err := e.PlaybooksAdminClient.Stats.GetSiteStats(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, stats)
|
|
||||||
assert.Equal(t, 6, stats.TotalPlaybooks)
|
|
||||||
assert.Equal(t, 3, stats.TotalPlaybookRuns)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlaybookKeyMetricsStats(t *testing.T) {
|
|
||||||
e := Setup(t)
|
|
||||||
e.CreateBasic()
|
|
||||||
|
|
||||||
t.Run("3 runs with published metrics, 2 runs without publishing", func(t *testing.T) {
|
|
||||||
playbookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{
|
|
||||||
Title: "pb1",
|
|
||||||
TeamID: e.BasicTeam.Id,
|
|
||||||
Public: true,
|
|
||||||
Metrics: createMetricsConfigs([]string{client.MetricTypeCurrency, client.MetricTypeDuration}),
|
|
||||||
})
|
|
||||||
require.NoError(e.T, err)
|
|
||||||
|
|
||||||
pb, err := e.PlaybooksClient.Playbooks.Get(context.Background(), playbookID)
|
|
||||||
require.NoError(e.T, err)
|
|
||||||
|
|
||||||
metricsData := createMetricsData(pb.Metrics, [][]int64{{12312, 9123}, {653, 7262}, {322, 76575}})
|
|
||||||
// create runs and publish metrics data
|
|
||||||
createRunsWithMetrics(t, e, playbookID, metricsData, true)
|
|
||||||
// create runs, set metrics data, but do not publish
|
|
||||||
createRunsWithMetrics(t, e, playbookID, metricsData[1:], false)
|
|
||||||
|
|
||||||
stats, err := e.PlaybooksClient.Playbooks.Stats(context.Background(), playbookID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, stats.MetricOverallAverage, intsToNullInts([]int64{4429, 30986}))
|
|
||||||
require.Equal(t, stats.MetricRollingAverage, intsToNullInts([]int64{4429, 30986}))
|
|
||||||
require.Equal(t, stats.MetricRollingAverageChange, []null.Int{null.NewInt(0, false), null.NewInt(0, false)})
|
|
||||||
require.Equal(t, stats.MetricRollingValues, [][]int64{{322, 653, 12312}, {76575, 7262, 9123}})
|
|
||||||
require.Equal(t, stats.MetricValueRange, [][]int64{{322, 12312}, {7262, 76575}})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("13 runs with published metrics, 7 runs without publishing", func(t *testing.T) {
|
|
||||||
playbookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{
|
|
||||||
Title: "pb2",
|
|
||||||
TeamID: e.BasicTeam.Id,
|
|
||||||
Public: true,
|
|
||||||
Metrics: createMetricsConfigs([]string{client.MetricTypeCurrency, client.MetricTypeInteger, client.MetricTypeDuration}),
|
|
||||||
})
|
|
||||||
require.NoError(e.T, err)
|
|
||||||
|
|
||||||
pb, err := e.PlaybooksClient.Playbooks.Get(context.Background(), playbookID)
|
|
||||||
require.NoError(e.T, err)
|
|
||||||
|
|
||||||
data := make([][]int64, 15)
|
|
||||||
for i := range data {
|
|
||||||
data[i] = []int64{100 + int64(i), 2000000 + int64(i), 3000000000 + int64(i)}
|
|
||||||
}
|
|
||||||
metricsData := createMetricsData(pb.Metrics, data)
|
|
||||||
createRunsWithMetrics(t, e, playbookID, metricsData, true)
|
|
||||||
createRunsWithMetrics(t, e, playbookID, metricsData[8:], false)
|
|
||||||
|
|
||||||
stats, err := e.PlaybooksClient.Playbooks.Stats(context.Background(), playbookID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, stats.MetricOverallAverage, intsToNullInts([]int64{107, 2000007, 3000000007}))
|
|
||||||
require.Equal(t, stats.MetricRollingAverage, intsToNullInts([]int64{109, 2000009, 3000000009})) // last 10 runs average
|
|
||||||
require.Equal(t, stats.MetricRollingAverageChange, intsToNullInts([]int64{6, 0, 0}))
|
|
||||||
require.Equal(t, stats.MetricRollingValues,
|
|
||||||
[][]int64{
|
|
||||||
{114, 113, 112, 111, 110, 109, 108, 107, 106, 105},
|
|
||||||
{2000014, 2000013, 2000012, 2000011, 2000010, 2000009, 2000008, 2000007, 2000006, 2000005},
|
|
||||||
{3000000014, 3000000013, 3000000012, 3000000011, 3000000010, 3000000009, 3000000008, 3000000007, 3000000006, 3000000005},
|
|
||||||
})
|
|
||||||
require.Equal(t, stats.MetricValueRange, [][]int64{{100, 114}, {2000000, 2000014}, {3000000000, 3000000014}})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("23 runs with published metrics", func(t *testing.T) {
|
|
||||||
playbookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{
|
|
||||||
Title: "pb3",
|
|
||||||
TeamID: e.BasicTeam.Id,
|
|
||||||
Public: true,
|
|
||||||
Metrics: createMetricsConfigs([]string{client.MetricTypeCurrency}),
|
|
||||||
})
|
|
||||||
require.NoError(e.T, err)
|
|
||||||
|
|
||||||
pb, err := e.PlaybooksClient.Playbooks.Get(context.Background(), playbookID)
|
|
||||||
require.NoError(e.T, err)
|
|
||||||
|
|
||||||
data := make([][]int64, 23)
|
|
||||||
for i := range data {
|
|
||||||
data[i] = []int64{10 + int64(i)} //11, 12, 13 ... 32
|
|
||||||
}
|
|
||||||
metricsData := createMetricsData(pb.Metrics, data)
|
|
||||||
createRunsWithMetrics(t, e, playbookID, metricsData, true)
|
|
||||||
|
|
||||||
stats, err := e.PlaybooksClient.Playbooks.Stats(context.Background(), playbookID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, stats.MetricOverallAverage, intsToNullInts([]int64{21}))
|
|
||||||
require.Equal(t, stats.MetricRollingAverage, intsToNullInts([]int64{27})) // last 10 runs average
|
|
||||||
require.Equal(t, stats.MetricRollingAverageChange, intsToNullInts([]int64{58}))
|
|
||||||
require.Equal(t, stats.MetricRollingValues, [][]int64{{32, 31, 30, 29, 28, 27, 26, 25, 24, 23}})
|
|
||||||
require.Equal(t, stats.MetricValueRange, [][]int64{{10, 32}})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("publish runs with metrics, then add additional metric to the playbook", func(t *testing.T) {
|
|
||||||
playbookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{
|
|
||||||
Title: "pb4",
|
|
||||||
TeamID: e.BasicTeam.Id,
|
|
||||||
Public: true,
|
|
||||||
Metrics: createMetricsConfigs([]string{client.MetricTypeCurrency}),
|
|
||||||
})
|
|
||||||
require.NoError(e.T, err)
|
|
||||||
|
|
||||||
pb, err := e.PlaybooksClient.Playbooks.Get(context.Background(), playbookID)
|
|
||||||
require.NoError(e.T, err)
|
|
||||||
|
|
||||||
metricsData := createMetricsData(pb.Metrics, [][]int64{{2}, {1}, {2}, {7}, {3}, {5}, {1}, {7}, {2}, {3}, {5}, {6}, {7}, {1}})
|
|
||||||
createRunsWithMetrics(t, e, playbookID, metricsData, true)
|
|
||||||
|
|
||||||
// add a metric to the playbook at first position
|
|
||||||
pb.Metrics = append(pb.Metrics, pb.Metrics[0])
|
|
||||||
pb.Metrics[0] = client.PlaybookMetricConfig{
|
|
||||||
Title: "metric2",
|
|
||||||
Type: client.MetricTypeInteger,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = e.PlaybooksClient.Playbooks.Update(context.Background(), *pb)
|
|
||||||
require.NoError(e.T, err)
|
|
||||||
|
|
||||||
stats, err := e.PlaybooksClient.Playbooks.Stats(context.Background(), playbookID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, stats.MetricOverallAverage, []null.Int{null.NewInt(0, false), null.IntFrom(3)})
|
|
||||||
require.Equal(t, stats.MetricRollingAverage, []null.Int{null.NewInt(0, false), null.IntFrom(4)}) // last 10 runs average
|
|
||||||
require.Equal(t, stats.MetricRollingAverageChange, []null.Int{null.NewInt(0, false), null.IntFrom(33)})
|
|
||||||
require.Equal(t, stats.MetricRollingValues, [][]int64{nil, {1, 7, 6, 5, 3, 2, 7, 1, 5, 3}})
|
|
||||||
require.Equal(t, stats.MetricValueRange, [][]int64{nil, {1, 7}})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func createRunsWithMetrics(t *testing.T, e *TestEnvironment, playbookID string, metricsData [][]client.RunMetricData, publish bool) {
|
|
||||||
for i, md := range metricsData {
|
|
||||||
run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{
|
|
||||||
Name: fmt.Sprint("run", i),
|
|
||||||
OwnerUserID: e.RegularUser.Id,
|
|
||||||
TeamID: e.BasicTeam.Id,
|
|
||||||
PlaybookID: playbookID,
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, run)
|
|
||||||
|
|
||||||
retrospective := client.RetrospectiveUpdate{
|
|
||||||
Text: fmt.Sprint("retro text", i),
|
|
||||||
Metrics: md,
|
|
||||||
}
|
|
||||||
|
|
||||||
//publish or save retro info
|
|
||||||
if publish {
|
|
||||||
err = e.PlaybooksClient.PlaybookRuns.PublishRetrospective(context.Background(), run.ID, e.RegularUser.Id, retrospective)
|
|
||||||
} else {
|
|
||||||
err = e.PlaybooksClient.PlaybookRuns.UpdateRetrospective(context.Background(), run.ID, e.RegularUser.Id, retrospective)
|
|
||||||
}
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createMetricsData(metricsConfigs []client.PlaybookMetricConfig, data [][]int64) [][]client.RunMetricData {
|
|
||||||
metricsData := make([][]client.RunMetricData, len(data))
|
|
||||||
for i, d := range data {
|
|
||||||
md := make([]client.RunMetricData, len(metricsConfigs))
|
|
||||||
for j, c := range metricsConfigs {
|
|
||||||
md[j] = client.RunMetricData{MetricConfigID: c.ID, Value: null.IntFrom(d[j])}
|
|
||||||
}
|
|
||||||
metricsData[i] = md
|
|
||||||
}
|
|
||||||
return metricsData
|
|
||||||
}
|
|
||||||
|
|
||||||
func createMetricsConfigs(types []string) []client.PlaybookMetricConfig {
|
|
||||||
configs := make([]client.PlaybookMetricConfig, len(types))
|
|
||||||
for i, t := range types {
|
|
||||||
configs[i] = client.PlaybookMetricConfig{
|
|
||||||
Title: fmt.Sprint("metric", i),
|
|
||||||
Type: t,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return configs
|
|
||||||
}
|
|
||||||
|
|
||||||
func intsToNullInts(nums []int64) []null.Int {
|
|
||||||
res := make([]null.Int, len(nums))
|
|
||||||
for i := range nums {
|
|
||||||
res[i] = null.IntFrom(nums[i])
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCreateEvent(t *testing.T) {
|
|
||||||
e := Setup(t)
|
|
||||||
e.CreateBasic()
|
|
||||||
|
|
||||||
t.Run("create an event with bad type fails", func(t *testing.T) {
|
|
||||||
err := e.PlaybooksClient.Telemetry.CreateEvent(context.Background(), "run_status_update", "bad_type", nil)
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create an event with bad name fails", func(t *testing.T) {
|
|
||||||
err := e.PlaybooksClient.Telemetry.CreateEvent(context.Background(), "bad_name", "page", nil)
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create an event correctly with no extra data", func(t *testing.T) {
|
|
||||||
err := e.PlaybooksClient.Telemetry.CreateEvent(context.Background(), "run_status_update", "page", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("create an event correctly with extra data", func(t *testing.T) {
|
|
||||||
extra := map[string]interface{}{
|
|
||||||
"foo": "bar",
|
|
||||||
"baz": 5,
|
|
||||||
}
|
|
||||||
err := e.PlaybooksClient.Telemetry.CreateEvent(context.Background(), "run_status_update", "page", extra)
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import "github.com/mattermost/mattermost/server/public/model"
|
|
||||||
|
|
||||||
type GenericChannelActionWithoutPayload struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
ChannelID string `json:"channel_id"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
DeleteAt int64 `json:"delete_at"`
|
|
||||||
ActionType ActionType `json:"action_type"`
|
|
||||||
TriggerType TriggerType `json:"trigger_type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GenericChannelAction struct {
|
|
||||||
GenericChannelActionWithoutPayload
|
|
||||||
Payload interface{} `json:"payload"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WelcomeMessagePayload struct {
|
|
||||||
Message string `json:"message" mapstructure:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PromptRunPlaybookFromKeywordsPayload struct {
|
|
||||||
Keywords []string `json:"keywords" mapstructure:"keywords"`
|
|
||||||
PlaybookID string `json:"playbook_id" mapstructure:"playbook_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CategorizeChannelPayload struct {
|
|
||||||
CategoryName string `json:"category_name" mapstructure:"category_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActionType string
|
|
||||||
type TriggerType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Action types: add new types to the ValidTriggerTypes array below
|
|
||||||
ActionTypeWelcomeMessage ActionType = "send_welcome_message"
|
|
||||||
ActionTypePromptRunPlaybook ActionType = "prompt_run_playbook"
|
|
||||||
ActionTypeCategorizeChannel ActionType = "categorize_channel"
|
|
||||||
|
|
||||||
// Trigger types: add new types to the ValidTriggerTypes array below
|
|
||||||
TriggerTypeNewMemberJoins TriggerType = "new_member_joins"
|
|
||||||
TriggerTypeKeywordsPosted TriggerType = "keywords"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ValidActionTypes = []ActionType{
|
|
||||||
ActionTypeWelcomeMessage,
|
|
||||||
ActionTypePromptRunPlaybook,
|
|
||||||
ActionTypeCategorizeChannel,
|
|
||||||
}
|
|
||||||
|
|
||||||
var ValidTriggerTypes = []TriggerType{
|
|
||||||
TriggerTypeNewMemberJoins,
|
|
||||||
TriggerTypeKeywordsPosted,
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetChannelActionOptions struct {
|
|
||||||
ActionType ActionType
|
|
||||||
TriggerType TriggerType
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChannelActionService interface {
|
|
||||||
// Create creates a new action
|
|
||||||
Create(action GenericChannelAction) (string, error)
|
|
||||||
|
|
||||||
// Get returns the action identified by id
|
|
||||||
Get(id string) (GenericChannelAction, error)
|
|
||||||
|
|
||||||
// GetChannelActions returns all actions in channelID,
|
|
||||||
// filtered with the options if different from its zero value
|
|
||||||
GetChannelActions(channelID string, options GetChannelActionOptions) ([]GenericChannelAction, error)
|
|
||||||
|
|
||||||
// Validate checks that the action type, trigger type and
|
|
||||||
// payload are all valid and consistent with each other
|
|
||||||
Validate(action GenericChannelAction) error
|
|
||||||
|
|
||||||
// Update updates an existing action identified by action.ID
|
|
||||||
Update(action GenericChannelAction, userID string) error
|
|
||||||
|
|
||||||
// UserHasJoinedChannel is called when userID has joined channelID. If actorID is not blank, userID
|
|
||||||
// was invited by actorID.
|
|
||||||
UserHasJoinedChannel(userID, channelID, actorID string)
|
|
||||||
|
|
||||||
// CheckAndSendMessageOnJoin checks if userID has viewed channelID and sends
|
|
||||||
// the registered welcome message action. Returns true if the message was sent.
|
|
||||||
CheckAndSendMessageOnJoin(userID, channelID string) bool
|
|
||||||
|
|
||||||
// MessageHasBeenPosted suggests playbooks to the user if triggered
|
|
||||||
MessageHasBeenPosted(post *model.Post)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChannelActionStore interface {
|
|
||||||
// Create creates a new action
|
|
||||||
Create(action GenericChannelAction) (string, error)
|
|
||||||
|
|
||||||
// Get returns the action identified by id
|
|
||||||
Get(id string) (GenericChannelAction, error)
|
|
||||||
|
|
||||||
// GetChannelActions returns all actions in channelID,
|
|
||||||
// filtered with the options if different from its zero value
|
|
||||||
GetChannelActions(channelID string, options GetChannelActionOptions) ([]GenericChannelAction, error)
|
|
||||||
|
|
||||||
// Update updates an existing action identified by action.ID
|
|
||||||
Update(action GenericChannelAction) error
|
|
||||||
|
|
||||||
// HasViewedChannel returns true if userID has viewed channelID
|
|
||||||
HasViewedChannel(userID, channelID string) bool
|
|
||||||
|
|
||||||
// SetViewedChannel records that userID has viewed channelID. NOTE: does not check if there is already a
|
|
||||||
// record of that userID/channelID (i.e., will create duplicate rows)
|
|
||||||
SetViewedChannel(userID, channelID string) error
|
|
||||||
|
|
||||||
// SetViewedChannel records that all users in userIDs have viewed channelID.
|
|
||||||
SetMultipleViewedChannel(userIDs []string, channelID string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChannelActionTelemetry defines the methods that the ChannelAction service needs from RudderTelemetry.
|
|
||||||
// userID is the user initiating the event.
|
|
||||||
type ChannelActionTelemetry interface {
|
|
||||||
// RunChannelAction tracks the execution of a channel action, performed by the specified user.
|
|
||||||
RunChannelAction(action GenericChannelAction, userID string)
|
|
||||||
|
|
||||||
// UpdateChannelAction tracks the update of a channel action, performed by the specified user.
|
|
||||||
UpdateChannelAction(action GenericChannelAction, userID string)
|
|
||||||
}
|
|
||||||
@@ -1,578 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/bot"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/config"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PlaybookGetter interface {
|
|
||||||
Get(id string) (Playbook, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type channelActionServiceImpl struct {
|
|
||||||
poster bot.Poster
|
|
||||||
configService config.Service
|
|
||||||
store ChannelActionStore
|
|
||||||
api playbooks.ServicesAPI
|
|
||||||
playbookGetter PlaybookGetter
|
|
||||||
keywordsThreadIgnorer KeywordsThreadIgnorer
|
|
||||||
telemetry ChannelActionTelemetry
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewChannelActionsService(api playbooks.ServicesAPI, poster bot.Poster, configService config.Service, store ChannelActionStore, playbookGetter PlaybookGetter, keywordsThreadIgnorer KeywordsThreadIgnorer, telemetry ChannelActionTelemetry) ChannelActionService {
|
|
||||||
return &channelActionServiceImpl{
|
|
||||||
poster: poster,
|
|
||||||
configService: configService,
|
|
||||||
store: store,
|
|
||||||
api: api,
|
|
||||||
playbookGetter: playbookGetter,
|
|
||||||
keywordsThreadIgnorer: keywordsThreadIgnorer,
|
|
||||||
telemetry: telemetry,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setViewedChannelForEveryMember mark channelID as viewed for all its existing members
|
|
||||||
func (a *channelActionServiceImpl) setViewedChannelForEveryMember(channelID string) error {
|
|
||||||
// TODO: this is a magic number, we should load test this function to find a
|
|
||||||
// good threshold to share the workload between the goroutines
|
|
||||||
perPage := 200
|
|
||||||
|
|
||||||
page := 0
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
var goroutineErr error
|
|
||||||
|
|
||||||
for {
|
|
||||||
members, err := a.api.GetChannelMembers(channelID, page, perPage)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to retrieve members of channel with ID %q", channelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(members) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
userIDs := make([]string, 0, len(members))
|
|
||||||
for _, member := range members {
|
|
||||||
userIDs = append(userIDs, member.UserId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.store.SetMultipleViewedChannel(userIDs, channelID); err != nil {
|
|
||||||
// We don't care whether multiple goroutines assign this value, as we're
|
|
||||||
// only interested in knowing if there was at least one error
|
|
||||||
goroutineErr = errors.Wrapf(err, "unable to mark channel with ID %q as viewed for users %v", channelID, userIDs)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
page++
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
return goroutineErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *channelActionServiceImpl) Create(action GenericChannelAction) (string, error) {
|
|
||||||
actions, err := a.store.GetChannelActions(action.ChannelID, GetChannelActionOptions{
|
|
||||||
ActionType: action.ActionType,
|
|
||||||
TriggerType: action.TriggerType,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(actions) > 0 {
|
|
||||||
return "", fmt.Errorf("only one action of action type %q and trigger type %q is allowed", string(action.ActionType), string(action.TriggerType))
|
|
||||||
}
|
|
||||||
|
|
||||||
if action.ActionType == ActionTypeWelcomeMessage && action.Enabled {
|
|
||||||
if err := a.setViewedChannelForEveryMember(action.ChannelID); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.store.Create(action)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *channelActionServiceImpl) Get(id string) (GenericChannelAction, error) {
|
|
||||||
return a.store.Get(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *channelActionServiceImpl) GetChannelActions(channelID string, options GetChannelActionOptions) ([]GenericChannelAction, error) {
|
|
||||||
return a.store.GetChannelActions(channelID, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *channelActionServiceImpl) Validate(action GenericChannelAction) error {
|
|
||||||
// Validate the trigger type and action types
|
|
||||||
switch action.TriggerType {
|
|
||||||
case TriggerTypeNewMemberJoins:
|
|
||||||
switch action.ActionType {
|
|
||||||
case ActionTypeWelcomeMessage:
|
|
||||||
break
|
|
||||||
case ActionTypeCategorizeChannel:
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("action type %q is not valid for trigger type %q", action.ActionType, action.TriggerType)
|
|
||||||
}
|
|
||||||
case TriggerTypeKeywordsPosted:
|
|
||||||
if action.ActionType != ActionTypePromptRunPlaybook {
|
|
||||||
return fmt.Errorf("action type %q is not valid for trigger type %q", action.ActionType, action.TriggerType)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("trigger type %q not recognized", action.TriggerType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the payload depending on the action type
|
|
||||||
switch action.ActionType {
|
|
||||||
case ActionTypeWelcomeMessage:
|
|
||||||
var payload WelcomeMessagePayload
|
|
||||||
if err := mapstructure.Decode(action.Payload, &payload); err != nil {
|
|
||||||
return fmt.Errorf("unable to decode payload from action")
|
|
||||||
}
|
|
||||||
case ActionTypePromptRunPlaybook:
|
|
||||||
var payload PromptRunPlaybookFromKeywordsPayload
|
|
||||||
if err := mapstructure.Decode(action.Payload, &payload); err != nil {
|
|
||||||
return fmt.Errorf("unable to decode payload from action")
|
|
||||||
}
|
|
||||||
if err := checkValidPromptRunPlaybookFromKeywordsPayload(payload); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case ActionTypeCategorizeChannel:
|
|
||||||
var payload CategorizeChannelPayload
|
|
||||||
if err := mapstructure.Decode(action.Payload, &payload); err != nil {
|
|
||||||
return fmt.Errorf("unable to decode payload from action")
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("action type %q not recognized", action.ActionType)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkValidPromptRunPlaybookFromKeywordsPayload(payload PromptRunPlaybookFromKeywordsPayload) error {
|
|
||||||
for _, keyword := range payload.Keywords {
|
|
||||||
if keyword == "" {
|
|
||||||
return fmt.Errorf("payload field 'keywords' must contain only non-empty keywords")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload.PlaybookID != "" && !model.IsValidId(payload.PlaybookID) {
|
|
||||||
return fmt.Errorf("payload field 'playbook_id' must be a valid ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *channelActionServiceImpl) Update(action GenericChannelAction, userID string) error {
|
|
||||||
oldAction, err := a.Get(action.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to retrieve existing action with ID %q", action.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if action.ActionType == ActionTypeWelcomeMessage && !oldAction.Enabled && action.Enabled {
|
|
||||||
if err := a.setViewedChannelForEveryMember(action.ChannelID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.store.Update(action); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
a.telemetry.UpdateChannelAction(action, userID)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserHasJoinedChannel is called when userID has joined channelID. If actorID is not blank, userID
|
|
||||||
// was invited by actorID.
|
|
||||||
func (a *channelActionServiceImpl) UserHasJoinedChannel(userID, channelID, actorID string) {
|
|
||||||
user, err := a.api.GetUserByID(userID)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).WithField("user_id", userID).Error("failed to resolve user")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
channel, err := a.api.GetChannelByID(channelID)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).WithField("channel_id", channelID).Error("failed to resolve channel")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.IsBot {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
actions, err := a.GetChannelActions(channelID, GetChannelActionOptions{
|
|
||||||
ActionType: ActionTypeCategorizeChannel,
|
|
||||||
TriggerType: TriggerTypeNewMemberJoins,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).WithField("channel_id", channelID).Error("failed to get the channel actions")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(actions) > 1 {
|
|
||||||
logrus.WithFields(logrus.Fields{
|
|
||||||
"action_type": ActionTypeCategorizeChannel,
|
|
||||||
"trigger_type": TriggerTypeNewMemberJoins,
|
|
||||||
"num_actions": len(actions),
|
|
||||||
}).Error("expected only one action to be retrieved")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(actions) != 1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
action := actions[0]
|
|
||||||
if !action.Enabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload CategorizeChannelPayload
|
|
||||||
if err = mapstructure.Decode(action.Payload, &payload); err != nil {
|
|
||||||
logrus.WithError(err).Error("unable to decode payload of CategorizeChannelPayload")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload.CategoryName != "" {
|
|
||||||
// Update sidebar category in the go-routine not to block the UserHasJoinedChannel hook
|
|
||||||
go func() {
|
|
||||||
// Wait for 5 seconds(a magic number) for the webapp to get the `user_added` event,
|
|
||||||
// finish channel categorization and update it's state in redux.
|
|
||||||
// Currently there is no way to detect when webapp finishes the job.
|
|
||||||
// After that we can update the categories safely.
|
|
||||||
// Technically if user starts multiple runs simultaneously we will still get the race condition
|
|
||||||
// on category update. Since that's not realistic at the moment we are not adding the
|
|
||||||
// distributed lock here.
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
|
|
||||||
err = a.createOrUpdatePlaybookRunSidebarCategory(userID, channelID, channel.TeamId, payload.CategoryName)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Error("failed to categorize channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
a.telemetry.RunChannelAction(action, userID)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// createOrUpdatePlaybookRunSidebarCategory creates or updates a "Playbook Runs" sidebar category if
|
|
||||||
// it does not already exist and adds the channel within the sidebar category
|
|
||||||
func (a *channelActionServiceImpl) createOrUpdatePlaybookRunSidebarCategory(userID, channelID, teamID, categoryName string) error {
|
|
||||||
sidebar, err := a.api.GetChannelSidebarCategories(userID, teamID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var categoryID string
|
|
||||||
for _, category := range sidebar.Categories {
|
|
||||||
if strings.EqualFold(category.DisplayName, categoryName) {
|
|
||||||
categoryID = category.Id
|
|
||||||
if !sliceContains(category.Channels, channelID) {
|
|
||||||
category.Channels = append(category.Channels, channelID)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if categoryID == "" {
|
|
||||||
_, err = a.api.CreateChannelSidebarCategory(userID, teamID, &model.SidebarCategoryWithChannels{
|
|
||||||
SidebarCategory: model.SidebarCategory{
|
|
||||||
UserId: userID,
|
|
||||||
TeamId: teamID,
|
|
||||||
DisplayName: categoryName,
|
|
||||||
Muted: false,
|
|
||||||
},
|
|
||||||
Channels: []string{channelID},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove channel from previous category
|
|
||||||
for _, category := range sidebar.Categories {
|
|
||||||
if strings.EqualFold(category.DisplayName, categoryName) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for i, channel := range category.Channels {
|
|
||||||
if channel == channelID {
|
|
||||||
category.Channels = append(category.Channels[:i], category.Channels[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = a.api.UpdateChannelSidebarCategories(userID, teamID, sidebar.Categories)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sliceContains(strs []string, target string) bool {
|
|
||||||
for _, s := range strs {
|
|
||||||
if s == target {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckAndSendMessageOnJoin checks if userID has viewed channelID and sends
|
|
||||||
// playbookRun.MessageOnJoin if it exists. Returns true if the message was sent.
|
|
||||||
func (a *channelActionServiceImpl) CheckAndSendMessageOnJoin(userID, channelID string) bool {
|
|
||||||
hasViewed := a.store.HasViewedChannel(userID, channelID)
|
|
||||||
|
|
||||||
if hasViewed {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
actions, err := a.store.GetChannelActions(channelID, GetChannelActionOptions{
|
|
||||||
TriggerType: TriggerTypeNewMemberJoins,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).WithFields(logrus.Fields{
|
|
||||||
"channel_id": channelID,
|
|
||||||
"trigger_type": TriggerTypeNewMemberJoins,
|
|
||||||
}).Error("failed to resolve actions")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = a.store.SetViewedChannel(userID, channelID); err != nil {
|
|
||||||
// If duplicate entry, userID has viewed channelID. If not a duplicate, assume they haven't.
|
|
||||||
return errors.Is(err, ErrDuplicateEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for the ActionTypeWelcomeMessage action
|
|
||||||
for _, action := range actions {
|
|
||||||
if action.ActionType == ActionTypeWelcomeMessage {
|
|
||||||
var payload WelcomeMessagePayload
|
|
||||||
if err := mapstructure.Decode(action.Payload, &payload); err != nil {
|
|
||||||
logrus.WithError(err).WithField("action_type", action.ActionType).Error("payload of action is not valid")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the action
|
|
||||||
a.poster.SystemEphemeralPost(userID, channelID, &model.Post{
|
|
||||||
Message: payload.Message,
|
|
||||||
})
|
|
||||||
|
|
||||||
a.telemetry.RunChannelAction(action, userID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *channelActionServiceImpl) MessageHasBeenPosted(post *model.Post) {
|
|
||||||
if post.IsSystemMessage() || a.keywordsThreadIgnorer.IsIgnored(post.RootId, post.UserId) || a.poster.IsFromPoster(post) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
actions, err := a.GetChannelActions(post.ChannelId, GetChannelActionOptions{
|
|
||||||
TriggerType: TriggerTypeKeywordsPosted,
|
|
||||||
ActionType: ActionTypePromptRunPlaybook,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).WithFields(logrus.Fields{
|
|
||||||
"channel_id": post.ChannelId,
|
|
||||||
"trigger_type": TriggerTypeKeywordsPosted,
|
|
||||||
}).Error("unable to retrieve channel actions")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finish early if there are no actions to prompt running a playbook
|
|
||||||
if len(actions) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
triggeredPlaybooksMap := make(map[string]Playbook)
|
|
||||||
presentTriggers := []string{}
|
|
||||||
for _, action := range actions {
|
|
||||||
if !action.Enabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload PromptRunPlaybookFromKeywordsPayload
|
|
||||||
if err := mapstructure.Decode(action.Payload, &payload); err != nil {
|
|
||||||
logrus.WithError(err).WithFields(logrus.Fields{
|
|
||||||
"payload": payload,
|
|
||||||
"actionType": action.ActionType,
|
|
||||||
"triggerType": action.TriggerType,
|
|
||||||
}).Error("unable to decode payload from action")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(payload.Keywords) == 0 || payload.PlaybookID == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestedPlaybook, err := a.playbookGetter.Get(payload.PlaybookID)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).WithField("playbook_id", payload.PlaybookID).Error("unable to get playbook to run action")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
triggers := payload.Keywords
|
|
||||||
actionExecuted := false
|
|
||||||
for _, trigger := range triggers {
|
|
||||||
if strings.Contains(post.Message, trigger) || containsAttachments(post.Attachments(), trigger) {
|
|
||||||
triggeredPlaybooksMap[payload.PlaybookID] = suggestedPlaybook
|
|
||||||
presentTriggers = append(presentTriggers, trigger)
|
|
||||||
actionExecuted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if actionExecuted {
|
|
||||||
a.telemetry.RunChannelAction(action, post.UserId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(triggeredPlaybooksMap) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
triggeredPlaybooks := []Playbook{}
|
|
||||||
for _, playbook := range triggeredPlaybooksMap {
|
|
||||||
triggeredPlaybooks = append(triggeredPlaybooks, playbook)
|
|
||||||
}
|
|
||||||
|
|
||||||
message := getPlaybookSuggestionsMessage(triggeredPlaybooks, presentTriggers)
|
|
||||||
attachment := getPlaybookSuggestionsSlackAttachment(triggeredPlaybooks, post.Id, "playbooks")
|
|
||||||
|
|
||||||
rootID := post.RootId
|
|
||||||
if rootID == "" {
|
|
||||||
rootID = post.Id
|
|
||||||
}
|
|
||||||
|
|
||||||
newPost := &model.Post{
|
|
||||||
Message: message,
|
|
||||||
ChannelId: post.ChannelId,
|
|
||||||
}
|
|
||||||
model.ParseSlackAttachment(newPost, []*model.SlackAttachment{attachment})
|
|
||||||
if err := a.poster.PostMessageToThread(rootID, newPost); err != nil {
|
|
||||||
logrus.WithError(err).Error("unable to post message with suggestions to run playbooks")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPlaybookSuggestionsMessage(suggestedPlaybooks []Playbook, triggers []string) string {
|
|
||||||
message := ""
|
|
||||||
triggerMessage := ""
|
|
||||||
if len(triggers) == 1 {
|
|
||||||
triggerMessage = fmt.Sprintf("`%s` is a trigger", triggers[0])
|
|
||||||
} else {
|
|
||||||
triggerMessage = fmt.Sprintf("`%s` are triggers", strings.Join(triggers, "`, `"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(suggestedPlaybooks) == 1 {
|
|
||||||
playbookURL := fmt.Sprintf("[%s](%s)", suggestedPlaybooks[0].Title, GetPlaybookDetailsRelativeURL(suggestedPlaybooks[0].ID))
|
|
||||||
message = fmt.Sprintf("%s for the %s playbook, would you like to run it?", triggerMessage, playbookURL)
|
|
||||||
} else {
|
|
||||||
message = fmt.Sprintf("%s for the multiple playbooks, would you like to run one of them?", triggerMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPlaybookSuggestionsSlackAttachment(playbooks []Playbook, triggeringPostID string, pluginID string) *model.SlackAttachment {
|
|
||||||
ignoreButton := &model.PostAction{
|
|
||||||
Id: "ignoreKeywordsButton",
|
|
||||||
Name: "No, ignore thread",
|
|
||||||
Type: model.PostActionTypeButton,
|
|
||||||
Integration: &model.PostActionIntegration{
|
|
||||||
URL: fmt.Sprintf("/plugins/%s/api/v0/signal/keywords/ignore-thread", pluginID),
|
|
||||||
Context: map[string]interface{}{
|
|
||||||
"postID": triggeringPostID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(playbooks) == 1 {
|
|
||||||
yesButton := &model.PostAction{
|
|
||||||
Id: "runPlaybookButton",
|
|
||||||
Name: "Yes, run playbook",
|
|
||||||
Type: model.PostActionTypeButton,
|
|
||||||
Integration: &model.PostActionIntegration{
|
|
||||||
URL: fmt.Sprintf("/plugins/%s/api/v0/signal/keywords/run-playbook", pluginID),
|
|
||||||
Context: map[string]interface{}{
|
|
||||||
"postID": triggeringPostID,
|
|
||||||
"selected_option": playbooks[0].ID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Style: "primary",
|
|
||||||
}
|
|
||||||
|
|
||||||
attachment := &model.SlackAttachment{
|
|
||||||
Actions: []*model.PostAction{yesButton, ignoreButton},
|
|
||||||
Text: "Open Channel Actions in the channel header to view and edit keywords.",
|
|
||||||
}
|
|
||||||
return attachment
|
|
||||||
}
|
|
||||||
|
|
||||||
options := []*model.PostActionOptions{}
|
|
||||||
for _, playbook := range playbooks {
|
|
||||||
option := &model.PostActionOptions{
|
|
||||||
Value: playbook.ID,
|
|
||||||
Text: playbook.Title,
|
|
||||||
}
|
|
||||||
options = append(options, option)
|
|
||||||
}
|
|
||||||
playbookChooser := &model.PostAction{
|
|
||||||
Id: "playbookChooser",
|
|
||||||
Name: "Select a playbook to run",
|
|
||||||
Type: model.PostActionTypeSelect,
|
|
||||||
Integration: &model.PostActionIntegration{
|
|
||||||
URL: fmt.Sprintf("/plugins/%s/api/v0/signal/keywords/run-playbook", pluginID),
|
|
||||||
Context: map[string]interface{}{
|
|
||||||
"postID": triggeringPostID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Options: options,
|
|
||||||
Style: "primary",
|
|
||||||
}
|
|
||||||
|
|
||||||
attachment := &model.SlackAttachment{
|
|
||||||
Actions: []*model.PostAction{playbookChooser, ignoreButton},
|
|
||||||
}
|
|
||||||
return attachment
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsAttachments(attachments []*model.SlackAttachment, trigger string) bool {
|
|
||||||
// Check PreText, Title, Text and Footer SlackAttachments fields for trigger.
|
|
||||||
for _, attachment := range attachments {
|
|
||||||
switch {
|
|
||||||
case strings.Contains(attachment.Pretext, trigger):
|
|
||||||
return true
|
|
||||||
case strings.Contains(attachment.Title, trigger):
|
|
||||||
return true
|
|
||||||
case strings.Contains(attachment.Text, trigger):
|
|
||||||
return true
|
|
||||||
case strings.Contains(attachment.Footer, trigger):
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CategoryItemType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
PlaybookItemType CategoryItemType = "p"
|
|
||||||
RunItemType CategoryItemType = "r"
|
|
||||||
)
|
|
||||||
|
|
||||||
func StringToItemType(item string) (CategoryItemType, error) {
|
|
||||||
var convertedItem CategoryItemType
|
|
||||||
if item == string(PlaybookItemType) {
|
|
||||||
convertedItem = PlaybookItemType
|
|
||||||
} else if item == string(RunItemType) {
|
|
||||||
convertedItem = RunItemType
|
|
||||||
} else {
|
|
||||||
return PlaybookItemType, errors.New("unknown item type")
|
|
||||||
}
|
|
||||||
return convertedItem, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type CategoryItem struct {
|
|
||||||
ItemID string `json:"item_id"`
|
|
||||||
Type CategoryItemType `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Public bool `json:"public"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Category represents sidebar category with items
|
|
||||||
type Category struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
TeamID string `json:"team_id"`
|
|
||||||
UserID string `json:"user_id"`
|
|
||||||
Collapsed bool `json:"collapsed"`
|
|
||||||
CreateAt int64 `json:"create_at"`
|
|
||||||
UpdateAt int64 `json:"update_at"`
|
|
||||||
DeleteAt int64 `json:"delete_at"`
|
|
||||||
Items []CategoryItem `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Category) IsValid() error {
|
|
||||||
if strings.TrimSpace(c.ID) == "" {
|
|
||||||
return errors.New("category ID cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.TrimSpace(c.Name) == "" {
|
|
||||||
return errors.New("category name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.TrimSpace(c.UserID) == "" {
|
|
||||||
return errors.New("category user ID cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.TrimSpace(c.TeamID) == "" {
|
|
||||||
return errors.New("category team id ID cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range c.Items {
|
|
||||||
if item.ItemID == "" {
|
|
||||||
return errors.New("item ID cannot be empty")
|
|
||||||
}
|
|
||||||
if item.Type != PlaybookItemType && item.Type != RunItemType {
|
|
||||||
return errors.New("item type is incorrect")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Category) ContainsItem(item CategoryItem) bool {
|
|
||||||
for _, catItem := range c.Items {
|
|
||||||
if catItem.ItemID == item.ItemID && catItem.Type == item.Type {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// CategoryService is the category service for managing categories
|
|
||||||
type CategoryService interface {
|
|
||||||
// Create creates a new Category
|
|
||||||
Create(category Category) (string, error)
|
|
||||||
|
|
||||||
// Get retrieves category with categoryID for user for team
|
|
||||||
Get(categoryID string) (Category, error)
|
|
||||||
|
|
||||||
// GetCategories retrieves all categories for user for team
|
|
||||||
GetCategories(teamID, userID string) ([]Category, error)
|
|
||||||
|
|
||||||
// Update updates a category
|
|
||||||
Update(category Category) error
|
|
||||||
|
|
||||||
// Delete deletes a category
|
|
||||||
Delete(categoryID string) error
|
|
||||||
|
|
||||||
// AddFavorite favorites an item, which may be either run or playbook
|
|
||||||
AddFavorite(item CategoryItem, teamID, userID string) error
|
|
||||||
|
|
||||||
// DeleteFavorite unfavorites an item, which may be either run or playbook
|
|
||||||
DeleteFavorite(item CategoryItem, teamID, userID string) error
|
|
||||||
|
|
||||||
// IsItemFavorite returns whether item was favorited or not
|
|
||||||
IsItemFavorite(item CategoryItem, teamID, userID string) (bool, error)
|
|
||||||
|
|
||||||
AreItemsFavorites(items []CategoryItem, teamID, userID string) ([]bool, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CategoryStore interface {
|
|
||||||
// Get retrieves a Category. Returns ErrNotFound if not found.
|
|
||||||
Get(id string) (Category, error)
|
|
||||||
|
|
||||||
// Create creates a new Category
|
|
||||||
Create(category Category) error
|
|
||||||
|
|
||||||
// GetCategories retrieves all categories for user for team
|
|
||||||
GetCategories(teamID, userID string) ([]Category, error)
|
|
||||||
|
|
||||||
// Update updates a category
|
|
||||||
Update(category Category) error
|
|
||||||
|
|
||||||
// Delete deletes a category
|
|
||||||
Delete(categoryID string) error
|
|
||||||
|
|
||||||
// GetFavoriteCategory returns favorite category
|
|
||||||
GetFavoriteCategory(teamID, userID string) (Category, error)
|
|
||||||
|
|
||||||
// AddItemToFavoriteCategory adds an item to favorite category,
|
|
||||||
// if favorite category does not exist it creates one
|
|
||||||
AddItemToFavoriteCategory(item CategoryItem, teamID, userID string) error
|
|
||||||
|
|
||||||
// AddItemToCategory adds an item to category
|
|
||||||
AddItemToCategory(item CategoryItem, categoryID string) error
|
|
||||||
|
|
||||||
// DeleteItemFromCategory adds an item to category
|
|
||||||
DeleteItemFromCategory(item CategoryItem, categoryID string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type CategoryTelemetry interface {
|
|
||||||
// FavoriteItem tracks run favoriting of an item. Item can be run or a playbook
|
|
||||||
FavoriteItem(item CategoryItem, userID string)
|
|
||||||
|
|
||||||
// UnfavoriteItem tracks run unfavoriting of an item. Item can be run or a playbook
|
|
||||||
UnfavoriteItem(item CategoryItem, userID string)
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type categoryService struct {
|
|
||||||
store CategoryStore
|
|
||||||
api playbooks.ServicesAPI
|
|
||||||
telemetry CategoryTelemetry
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPlaybookService returns a new playbook service
|
|
||||||
func NewCategoryService(store CategoryStore, api playbooks.ServicesAPI, categoryTelemetry CategoryTelemetry) CategoryService {
|
|
||||||
return &categoryService{
|
|
||||||
store: store,
|
|
||||||
api: api,
|
|
||||||
telemetry: categoryTelemetry,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create creates a new Category
|
|
||||||
func (c *categoryService) Create(category Category) (string, error) {
|
|
||||||
if category.ID != "" {
|
|
||||||
return "", errors.New("ID should be empty")
|
|
||||||
}
|
|
||||||
category.ID = model.NewId()
|
|
||||||
category.CreateAt = model.GetMillis()
|
|
||||||
category.UpdateAt = category.CreateAt
|
|
||||||
if err := category.IsValid(); err != nil {
|
|
||||||
return "", errors.Wrap(err, "invalid category")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.store.Create(category); err != nil {
|
|
||||||
return "", errors.Wrap(err, "Can't create category")
|
|
||||||
}
|
|
||||||
return category.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *categoryService) Get(categoryID string) (Category, error) {
|
|
||||||
category, err := c.store.Get(categoryID)
|
|
||||||
if err != nil {
|
|
||||||
return Category{}, errors.Wrap(err, "Can't get category")
|
|
||||||
}
|
|
||||||
return category, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCategories retrieves all categories for user for team
|
|
||||||
func (c *categoryService) GetCategories(teamID, userID string) ([]Category, error) {
|
|
||||||
if !model.IsValidId(teamID) {
|
|
||||||
return nil, errors.New("teamID is not valid")
|
|
||||||
}
|
|
||||||
if !model.IsValidId(userID) {
|
|
||||||
return nil, errors.New("userID is not valid")
|
|
||||||
}
|
|
||||||
return c.store.GetCategories(teamID, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update updates a category
|
|
||||||
func (c *categoryService) Update(category Category) error {
|
|
||||||
if category.ID == "" {
|
|
||||||
return errors.New("id should not be empty")
|
|
||||||
}
|
|
||||||
if category.Name == "" {
|
|
||||||
return errors.New("name should not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
category.UpdateAt = model.GetMillis()
|
|
||||||
if err := category.IsValid(); err != nil {
|
|
||||||
return errors.Wrap(err, "invalid category")
|
|
||||||
}
|
|
||||||
if err := c.store.Update(category); err != nil {
|
|
||||||
return errors.Wrap(err, "can't update category")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes a category
|
|
||||||
func (c *categoryService) Delete(categoryID string) error {
|
|
||||||
if err := c.store.Delete(categoryID); err != nil {
|
|
||||||
return errors.Wrap(err, "can't delete category")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddFavorite favorites an item, which may be either run or playbook
|
|
||||||
func (c *categoryService) AddFavorite(item CategoryItem, teamID, userID string) error {
|
|
||||||
if err := c.store.AddItemToFavoriteCategory(item, teamID, userID); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to add favorite")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.telemetry.FavoriteItem(item, userID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *categoryService) DeleteFavorite(item CategoryItem, teamID, userID string) error {
|
|
||||||
favoriteCategory, err := c.store.GetFavoriteCategory(teamID, userID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "can't get favorite category")
|
|
||||||
}
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, favItem := range favoriteCategory.Items {
|
|
||||||
if favItem.ItemID == item.ItemID && favItem.Type == item.Type {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return errors.New("Item is not favorited")
|
|
||||||
}
|
|
||||||
if err := c.store.DeleteItemFromCategory(item, favoriteCategory.ID); err != nil {
|
|
||||||
return errors.Wrap(err, "can't delete item from favorite category")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.telemetry.UnfavoriteItem(item, userID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *categoryService) IsItemFavorite(item CategoryItem, teamID, userID string) (bool, error) {
|
|
||||||
favoriteCategory, err := c.store.GetFavoriteCategory(teamID, userID)
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return false, nil
|
|
||||||
} else if err != nil {
|
|
||||||
return false, errors.Wrap(err, "can't get favorite category")
|
|
||||||
}
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, favItem := range favoriteCategory.Items {
|
|
||||||
if favItem.ItemID == item.ItemID && favItem.Type == item.Type {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return found, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *categoryService) AreItemsFavorites(items []CategoryItem, teamID, userID string) ([]bool, error) {
|
|
||||||
result := make([]bool, len(items))
|
|
||||||
|
|
||||||
favoriteCategory, err := c.store.GetFavoriteCategory(teamID, userID)
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return result, nil
|
|
||||||
} else if err != nil {
|
|
||||||
return result, errors.Wrap(err, "can't get favorite category")
|
|
||||||
}
|
|
||||||
|
|
||||||
categoryResult := make(map[CategoryItem]bool)
|
|
||||||
for _, favItem := range favoriteCategory.Items {
|
|
||||||
categoryResult[CategoryItem{
|
|
||||||
ItemID: favItem.ItemID,
|
|
||||||
Type: favItem.Type,
|
|
||||||
}] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, item := range items {
|
|
||||||
result[i] = categoryResult[item]
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import "github.com/pkg/errors"
|
|
||||||
|
|
||||||
// ErrNotFound used when an entity is not found.
|
|
||||||
var ErrNotFound = errors.New("not found")
|
|
||||||
|
|
||||||
// ErrChannelDisplayNameInvalid is used when a channel name is too long.
|
|
||||||
var ErrChannelDisplayNameInvalid = errors.New("channel name is invalid or too long")
|
|
||||||
|
|
||||||
// ErrPlaybookRunNotActive occurs when trying to run a command on a playbook run that has ended.
|
|
||||||
var ErrPlaybookRunNotActive = errors.New("already ended")
|
|
||||||
|
|
||||||
// ErrPlaybookRunActive occurs when trying to run a command on a playbook run that is active.
|
|
||||||
var ErrPlaybookRunActive = errors.New("already active")
|
|
||||||
|
|
||||||
// ErrMalformedPlaybookRun occurs when a playbook run is not valid.
|
|
||||||
var ErrMalformedPlaybookRun = errors.New("malformed")
|
|
||||||
|
|
||||||
// ErrDuplicateEntry occurs when failing to insert because the entry already existed.
|
|
||||||
var ErrDuplicateEntry = errors.New("duplicate entry")
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"reflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
const CurrentPlaybookExportVersion = 1
|
|
||||||
|
|
||||||
func getFieldsForExport(in interface{}) map[string]interface{} {
|
|
||||||
out := map[string]interface{}{}
|
|
||||||
|
|
||||||
inType := reflect.TypeOf(in)
|
|
||||||
inValue := reflect.ValueOf(in)
|
|
||||||
for i := 0; i < inType.NumField(); i++ {
|
|
||||||
field := inType.Field(i)
|
|
||||||
tag := field.Tag.Get("export")
|
|
||||||
fieldValue := inValue.Field(i)
|
|
||||||
if tag != "" && tag != "-" && !fieldValue.IsZero() {
|
|
||||||
out[tag] = fieldValue.Interface()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateChecklistItemExport(checklistItems []ChecklistItem) []interface{} {
|
|
||||||
exported := make([]interface{}, 0, len(checklistItems))
|
|
||||||
for _, item := range checklistItems {
|
|
||||||
exportItem := getFieldsForExport(item)
|
|
||||||
exported = append(exported, exportItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
return exported
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateChecklistExport(checklists []Checklist) []interface{} {
|
|
||||||
exported := make([]interface{}, 0, len(checklists))
|
|
||||||
for _, checklist := range checklists {
|
|
||||||
exportList := getFieldsForExport(checklist)
|
|
||||||
exportList["items"] = generateChecklistItemExport(checklist.Items)
|
|
||||||
exported = append(exported, exportList)
|
|
||||||
}
|
|
||||||
|
|
||||||
return exported
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateMetricsExport(metrics []PlaybookMetricConfig) []interface{} {
|
|
||||||
exported := make([]interface{}, 0, len(metrics))
|
|
||||||
for _, checklist := range metrics {
|
|
||||||
exportList := getFieldsForExport(checklist)
|
|
||||||
exported = append(exported, exportList)
|
|
||||||
}
|
|
||||||
|
|
||||||
return exported
|
|
||||||
}
|
|
||||||
|
|
||||||
// GeneratePlaybookExport returns a playbook in export format.
|
|
||||||
// Fields marked with the stuct tag "export" are included using the given string.
|
|
||||||
func GeneratePlaybookExport(playbook Playbook) ([]byte, error) {
|
|
||||||
export := getFieldsForExport(playbook)
|
|
||||||
export["version"] = CurrentPlaybookExportVersion
|
|
||||||
export["checklists"] = generateChecklistExport(playbook.Checklists)
|
|
||||||
export["metrics"] = generateMetricsExport(playbook.Metrics)
|
|
||||||
|
|
||||||
result, err := json.MarshalIndent(export, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gopkg.in/guregu/null.v4"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGeneratePlaybookExport(t *testing.T) {
|
|
||||||
pb := Playbook{
|
|
||||||
Title: "Testing",
|
|
||||||
CreateAt: 23423234,
|
|
||||||
Checklists: []Checklist{
|
|
||||||
{
|
|
||||||
Title: "checklist 1",
|
|
||||||
Items: []ChecklistItem{
|
|
||||||
{
|
|
||||||
Title: "This is an item",
|
|
||||||
Description: "It's an item",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Metrics: []PlaybookMetricConfig{
|
|
||||||
{
|
|
||||||
ID: "1",
|
|
||||||
PlaybookID: "11",
|
|
||||||
Title: "Title 1",
|
|
||||||
Description: "Description 1",
|
|
||||||
Type: MetricTypeCurrency,
|
|
||||||
Target: null.IntFrom(147),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := GeneratePlaybookExport(pb)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
result := Playbook{}
|
|
||||||
err = json.Unmarshal(output, &result)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Should copy the specified stuff
|
|
||||||
assert.Equal(t, result.Title, pb.Title)
|
|
||||||
|
|
||||||
// Shouldn't copy the not specificed stuff
|
|
||||||
assert.Equal(t, result.CreateAt, int64(0))
|
|
||||||
|
|
||||||
// Shouldn't copy metrics ID and PlaybookID fields
|
|
||||||
assert.NotEqual(t, result.Metrics, pb.Metrics)
|
|
||||||
//After cleaning ID and PlaybookID, should be equal
|
|
||||||
pb.Metrics[0].ID = ""
|
|
||||||
pb.Metrics[0].PlaybookID = ""
|
|
||||||
assert.Equal(t, result.Metrics, pb.Metrics)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func definesExports(t *testing.T, thing interface{}) {
|
|
||||||
inType := reflect.TypeOf(thing)
|
|
||||||
for i := 0; i < inType.NumField(); i++ {
|
|
||||||
field := inType.Field(i)
|
|
||||||
tag := strings.TrimSpace(field.Tag.Get("export"))
|
|
||||||
if tag == "" {
|
|
||||||
t.Errorf("%s struct does not define export for field %s. Please define this struct tag, see comment above playbook struct.", inType.Name(), field.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlaybookDefinesExports(t *testing.T) {
|
|
||||||
definesExports(t, Playbook{})
|
|
||||||
definesExports(t, Checklist{})
|
|
||||||
definesExports(t, ChecklistItem{})
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import "sync"
|
|
||||||
|
|
||||||
type KeywordsThreadIgnorer interface {
|
|
||||||
Ignore(postID, userID string)
|
|
||||||
IsIgnored(postID, userID string) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type keywordsThreadIgnorerImpl struct {
|
|
||||||
ignoredThreads map[string]map[string]bool // [postID][userID]
|
|
||||||
mutex sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewKeywordsThreadIgnorer() KeywordsThreadIgnorer {
|
|
||||||
return &keywordsThreadIgnorerImpl{
|
|
||||||
ignoredThreads: map[string]map[string]bool{},
|
|
||||||
mutex: sync.RWMutex{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignores ignores thread postID for the userID,
|
|
||||||
// other users will still get notifications in this thread
|
|
||||||
func (i *keywordsThreadIgnorerImpl) Ignore(postID, userID string) {
|
|
||||||
i.mutex.Lock()
|
|
||||||
defer i.mutex.Unlock()
|
|
||||||
if _, ok := i.ignoredThreads[postID]; !ok {
|
|
||||||
i.ignoredThreads[postID] = map[string]bool{}
|
|
||||||
}
|
|
||||||
i.ignoredThreads[postID][userID] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsIgnored checks whether this thread should be ignored for userID
|
|
||||||
func (i *keywordsThreadIgnorerImpl) IsIgnored(postID, userID string) bool {
|
|
||||||
i.mutex.RLock()
|
|
||||||
defer i.mutex.RUnlock()
|
|
||||||
if _, ok := i.ignoredThreads[postID]; !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return i.ignoredThreads[postID][userID]
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
// Code generated by MockGen. DO NOT EDIT.
|
|
||||||
// Source: github.com/mattermost/mattermost/server/v8/playbooks/server/app (interfaces: JobOnceScheduler)
|
|
||||||
|
|
||||||
// Package mock_app is a generated GoMock package.
|
|
||||||
package mock_app
|
|
||||||
|
|
||||||
import (
|
|
||||||
reflect "reflect"
|
|
||||||
time "time"
|
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
cluster "github.com/mattermost/mattermost/server/v8/playbooks/product/pluginapi/cluster"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockJobOnceScheduler is a mock of JobOnceScheduler interface.
|
|
||||||
type MockJobOnceScheduler struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockJobOnceSchedulerMockRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockJobOnceSchedulerMockRecorder is the mock recorder for MockJobOnceScheduler.
|
|
||||||
type MockJobOnceSchedulerMockRecorder struct {
|
|
||||||
mock *MockJobOnceScheduler
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockJobOnceScheduler creates a new mock instance.
|
|
||||||
func NewMockJobOnceScheduler(ctrl *gomock.Controller) *MockJobOnceScheduler {
|
|
||||||
mock := &MockJobOnceScheduler{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockJobOnceSchedulerMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
|
||||||
func (m *MockJobOnceScheduler) EXPECT() *MockJobOnceSchedulerMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel mocks base method.
|
|
||||||
func (m *MockJobOnceScheduler) Cancel(arg0 string) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
m.ctrl.Call(m, "Cancel", arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel indicates an expected call of Cancel.
|
|
||||||
func (mr *MockJobOnceSchedulerMockRecorder) Cancel(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cancel", reflect.TypeOf((*MockJobOnceScheduler)(nil).Cancel), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListScheduledJobs mocks base method.
|
|
||||||
func (m *MockJobOnceScheduler) ListScheduledJobs() ([]cluster.JobOnceMetadata, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ListScheduledJobs")
|
|
||||||
ret0, _ := ret[0].([]cluster.JobOnceMetadata)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListScheduledJobs indicates an expected call of ListScheduledJobs.
|
|
||||||
func (mr *MockJobOnceSchedulerMockRecorder) ListScheduledJobs() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListScheduledJobs", reflect.TypeOf((*MockJobOnceScheduler)(nil).ListScheduledJobs))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScheduleOnce mocks base method.
|
|
||||||
func (m *MockJobOnceScheduler) ScheduleOnce(arg0 string, arg1 time.Time) (*cluster.JobOnce, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ScheduleOnce", arg0, arg1)
|
|
||||||
ret0, _ := ret[0].(*cluster.JobOnce)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScheduleOnce indicates an expected call of ScheduleOnce.
|
|
||||||
func (mr *MockJobOnceSchedulerMockRecorder) ScheduleOnce(arg0, arg1 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScheduleOnce", reflect.TypeOf((*MockJobOnceScheduler)(nil).ScheduleOnce), arg0, arg1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCallback mocks base method.
|
|
||||||
func (m *MockJobOnceScheduler) SetCallback(arg0 func(string)) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "SetCallback", arg0)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCallback indicates an expected call of SetCallback.
|
|
||||||
func (mr *MockJobOnceSchedulerMockRecorder) SetCallback(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCallback", reflect.TypeOf((*MockJobOnceScheduler)(nil).SetCallback), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start mocks base method.
|
|
||||||
func (m *MockJobOnceScheduler) Start() error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Start")
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start indicates an expected call of Start.
|
|
||||||
func (mr *MockJobOnceSchedulerMockRecorder) Start() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockJobOnceScheduler)(nil).Start))
|
|
||||||
}
|
|
||||||
@@ -1,545 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/config"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrNoPermissions if the error is caused by the user not having permissions
|
|
||||||
var ErrNoPermissions = errors.New("does not have permissions")
|
|
||||||
|
|
||||||
// ErrLicensedFeature if the error is caused by the server not having the needed license for the feature
|
|
||||||
var ErrLicensedFeature = errors.New("not covered by current server license")
|
|
||||||
|
|
||||||
type LicenseChecker interface {
|
|
||||||
PlaybookAllowed(isPlaybookPublic bool) bool
|
|
||||||
RetrospectiveAllowed() bool
|
|
||||||
TimelineAllowed() bool
|
|
||||||
StatsAllowed() bool
|
|
||||||
ChecklistItemDueDateAllowed() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type PermissionsService struct {
|
|
||||||
playbookService PlaybookService
|
|
||||||
runService PlaybookRunService
|
|
||||||
api playbooks.ServicesAPI
|
|
||||||
configService config.Service
|
|
||||||
licenseChecker LicenseChecker
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPermissionsService(
|
|
||||||
playbookService PlaybookService,
|
|
||||||
runService PlaybookRunService,
|
|
||||||
api playbooks.ServicesAPI,
|
|
||||||
configService config.Service,
|
|
||||||
licenseChecker LicenseChecker,
|
|
||||||
) *PermissionsService {
|
|
||||||
return &PermissionsService{
|
|
||||||
playbookService,
|
|
||||||
runService,
|
|
||||||
api,
|
|
||||||
configService,
|
|
||||||
licenseChecker,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) PlaybookIsPublic(playbook Playbook) bool {
|
|
||||||
return playbook.Public
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) getPlaybookRole(userID string, playbook Playbook) []string {
|
|
||||||
if !p.canViewTeam(userID, playbook.TeamID) {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, member := range playbook.Members {
|
|
||||||
if member.UserID == userID {
|
|
||||||
return member.SchemeRoles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public playbooks
|
|
||||||
if playbook.Public {
|
|
||||||
// Public playbooks are public to those who can list channels on a team. (Not guests)
|
|
||||||
if p.api.HasPermissionToTeam(userID, playbook.TeamID, model.PermissionListTeamChannels) {
|
|
||||||
if playbook.DefaultPlaybookMemberRole == "" {
|
|
||||||
return []string{playbook.DefaultPlaybookMemberRole}
|
|
||||||
}
|
|
||||||
return []string{PlaybookRoleMember}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) hasPermissionsToPlaybook(userID string, playbook Playbook, permission *model.Permission) bool {
|
|
||||||
// Check at playbook level
|
|
||||||
if p.api.RolesGrantPermission(p.getPlaybookRole(userID, playbook), permission.Id) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cascade normally to higher level permissions
|
|
||||||
return p.api.HasPermissionToTeam(userID, playbook.TeamID, permission)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) HasPermissionsToRun(userID string, run *PlaybookRun, permission *model.Permission) bool {
|
|
||||||
// Check at run level
|
|
||||||
if err := p.runManagePropertiesWithPlaybookRun(userID, run); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cascade normally to higher level permissions
|
|
||||||
return p.api.HasPermissionToTeam(userID, run.TeamID, permission)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) canViewTeam(userID string, teamID string) bool {
|
|
||||||
if teamID == "" || userID == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is list team channels so that Guests are excluded.
|
|
||||||
return p.api.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) PlaybookCreate(userID string, playbook Playbook) error {
|
|
||||||
if !p.licenseChecker.PlaybookAllowed(p.PlaybookIsPublic(playbook)) {
|
|
||||||
return errors.Wrapf(ErrLicensedFeature, "the playbook is not valid with the current license")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the user has permissions over all broadcast channels
|
|
||||||
for _, channelID := range playbook.BroadcastChannelIDs {
|
|
||||||
if !p.api.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost) {
|
|
||||||
return errors.Errorf("user `%s` does not have permission to create posts in channel `%s`", userID, channelID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check all invited users have permissions to the team.
|
|
||||||
for _, userID := range playbook.InvitedUserIDs {
|
|
||||||
if !p.api.HasPermissionToTeam(userID, playbook.TeamID, model.PermissionViewTeam) {
|
|
||||||
return errors.Errorf(
|
|
||||||
"invited user `%s` does not have permission to playbook's team `%s`",
|
|
||||||
userID,
|
|
||||||
playbook.TeamID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Respect setting for not allowing mentions of a group.
|
|
||||||
for _, groupID := range playbook.InvitedGroupIDs {
|
|
||||||
group, err := p.api.GetGroup(groupID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "invalid group")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !group.AllowReference {
|
|
||||||
return errors.Errorf(
|
|
||||||
"group `%s` does not allow references",
|
|
||||||
groupID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check general permissions
|
|
||||||
permission := model.PermissionPrivatePlaybookCreate
|
|
||||||
if p.PlaybookIsPublic(playbook) {
|
|
||||||
permission = model.PermissionPublicPlaybookCreate
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.api.HasPermissionToTeam(userID, playbook.TeamID, permission) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to create playbook", userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) PlaybookManageProperties(userID string, playbook Playbook) error {
|
|
||||||
permission := model.PermissionPrivatePlaybookManageProperties
|
|
||||||
if p.PlaybookIsPublic(playbook) {
|
|
||||||
permission = model.PermissionPublicPlaybookManageProperties
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.hasPermissionsToPlaybook(userID, playbook, permission) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have access to playbook `%s`", userID, playbook.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaybookodifyWithFixes checks both ManageProperties and ManageMembers permissions
|
|
||||||
// performs permissions checks that can be resolved though modification of the input.
|
|
||||||
// This function modifies the playbook argument.
|
|
||||||
func (p *PermissionsService) PlaybookModifyWithFixes(userID string, playbook *Playbook, oldPlaybook Playbook) error {
|
|
||||||
// It is assumed that if you are calling this function there are properties changes
|
|
||||||
// This means that you need the manage properties permission to manage members for now.
|
|
||||||
if err := p.PlaybookManageProperties(userID, oldPlaybook); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.NoAddedBroadcastChannelsWithoutPermission(userID, playbook.BroadcastChannelIDs, oldPlaybook.BroadcastChannelIDs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredUsers := p.FilterInvitedUserIDs(playbook.InvitedUserIDs, playbook.TeamID)
|
|
||||||
playbook.InvitedUserIDs = filteredUsers
|
|
||||||
|
|
||||||
filteredGroups := p.FilterInvitedGroupIDs(playbook.InvitedGroupIDs)
|
|
||||||
playbook.InvitedGroupIDs = filteredGroups
|
|
||||||
|
|
||||||
if playbook.DefaultOwnerID != "" {
|
|
||||||
if !p.api.HasPermissionToTeam(playbook.DefaultOwnerID, playbook.TeamID, model.PermissionViewTeam) {
|
|
||||||
logrus.WithFields(logrus.Fields{
|
|
||||||
"team_id": playbook.TeamID,
|
|
||||||
"user_id": playbook.DefaultOwnerID,
|
|
||||||
}).Warn("owner is not a member of the playbook's team, disabling default owner")
|
|
||||||
playbook.DefaultOwnerID = ""
|
|
||||||
playbook.DefaultOwnerEnabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have changed members, if so check that permission.
|
|
||||||
if !reflect.DeepEqual(oldPlaybook.Members, playbook.Members) {
|
|
||||||
if err := p.PlaybookManageMembers(userID, oldPlaybook); err != nil {
|
|
||||||
return errors.Wrap(err, "attempted to modify members without permissions")
|
|
||||||
}
|
|
||||||
|
|
||||||
oldMemberRoles := map[string]string{}
|
|
||||||
for _, member := range oldPlaybook.Members {
|
|
||||||
oldMemberRoles[member.UserID] = strings.Join(member.Roles, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also need to check if roles changed. If so we need to check manage roles permission.
|
|
||||||
for _, member := range playbook.Members {
|
|
||||||
oldRoles, memberExisted := oldMemberRoles[member.UserID]
|
|
||||||
userAddedAsMember := !memberExisted && len(member.Roles) == 1 && member.Roles[0] == PlaybookRoleMember
|
|
||||||
rolesHaveNotChanged := memberExisted && strings.Join(member.Roles, ",") == oldRoles
|
|
||||||
if !(userAddedAsMember || rolesHaveNotChanged) {
|
|
||||||
if err := p.PlaybookManageRoles(userID, oldPlaybook); err != nil {
|
|
||||||
return errors.Wrap(err, "attempted to modify members without permissions")
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have done a public conversion
|
|
||||||
if oldPlaybook.Public != playbook.Public {
|
|
||||||
if oldPlaybook.Public {
|
|
||||||
if err := p.PlaybookMakePrivate(userID, oldPlaybook); err != nil {
|
|
||||||
return errors.Wrap(err, "attempted to make playbook private without permissions")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := p.PlaybookMakePublic(userID, oldPlaybook); err != nil {
|
|
||||||
return errors.Wrap(err, "attempted to make playbook public without permissions")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.licenseChecker.PlaybookAllowed(p.PlaybookIsPublic(*playbook)) {
|
|
||||||
return errors.Wrapf(ErrLicensedFeature, "the playbook is not valid with the current license")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) FilterInvitedUserIDs(invitedUserIDs []string, teamID string) []string {
|
|
||||||
filteredUsers := []string{}
|
|
||||||
for _, userID := range invitedUserIDs {
|
|
||||||
if !p.api.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
|
|
||||||
logrus.WithFields(logrus.Fields{
|
|
||||||
"team_id": teamID,
|
|
||||||
"user_id": userID,
|
|
||||||
}).Warn("user does not have permissions to playbook's team, removing from automated invite list")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filteredUsers = append(filteredUsers, userID)
|
|
||||||
}
|
|
||||||
return filteredUsers
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) FilterInvitedGroupIDs(invitedGroupIDs []string) []string {
|
|
||||||
filteredGroups := []string{}
|
|
||||||
for _, groupID := range invitedGroupIDs {
|
|
||||||
var group *model.Group
|
|
||||||
group, err := p.api.GetGroup(groupID)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithField("group_id", groupID).Error("failed to query group")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !group.AllowReference {
|
|
||||||
logrus.WithField("group_id", groupID).Warn("group does not allow references, removing from automated invite list")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredGroups = append(filteredGroups, groupID)
|
|
||||||
}
|
|
||||||
return filteredGroups
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) DeletePlaybook(userID string, playbook Playbook) error {
|
|
||||||
return p.PlaybookManageProperties(userID, playbook)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) NoAddedBroadcastChannelsWithoutPermission(userID string, broadcastChannelIDs, oldBroadcastChannelIDs []string) error {
|
|
||||||
oldChannelsSet := make(map[string]bool)
|
|
||||||
for _, channelID := range oldBroadcastChannelIDs {
|
|
||||||
oldChannelsSet[channelID] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, channelID := range broadcastChannelIDs {
|
|
||||||
if !oldChannelsSet[channelID] &&
|
|
||||||
!p.api.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost) {
|
|
||||||
return errors.Wrapf(
|
|
||||||
ErrNoPermissions,
|
|
||||||
"user `%s` does not have permission to create posts in channel `%s`",
|
|
||||||
userID,
|
|
||||||
channelID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) PlaybookManageMembers(userID string, playbook Playbook) error {
|
|
||||||
permission := model.PermissionPrivatePlaybookManageMembers
|
|
||||||
if p.PlaybookIsPublic(playbook) {
|
|
||||||
permission = model.PermissionPublicPlaybookManageMembers
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.hasPermissionsToPlaybook(userID, playbook, permission) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to manage members for playbook `%s`", userID, playbook.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) PlaybookManageRoles(userID string, playbook Playbook) error {
|
|
||||||
permission := model.PermissionPrivatePlaybookManageRoles
|
|
||||||
if p.PlaybookIsPublic(playbook) {
|
|
||||||
permission = model.PermissionPublicPlaybookManageRoles
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.hasPermissionsToPlaybook(userID, playbook, permission) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to manage roles for playbook `%s`", userID, playbook.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) PlaybookView(userID string, playbookID string) error {
|
|
||||||
playbook, err := p.playbookService.Get(playbookID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "Unable to get playbook to determine permissions, playbook id `%s`", playbookID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.PlaybookViewWithPlaybook(userID, playbook)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) PlaybookList(userID, teamID string) error {
|
|
||||||
// Can list playbooks if you are on the team
|
|
||||||
if p.canViewTeam(userID, teamID) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to list playbooks for team `%s`", userID, teamID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) PlaybookViewWithPlaybook(userID string, playbook Playbook) error {
|
|
||||||
noAccessErr := errors.Wrapf(
|
|
||||||
ErrNoPermissions,
|
|
||||||
"user `%s` to access playbook `%s`",
|
|
||||||
userID,
|
|
||||||
playbook.ID,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Playbooks are tied to teams. You must have permission to the team to have permission to the playbook.
|
|
||||||
if !p.canViewTeam(userID, playbook.TeamID) {
|
|
||||||
return errors.Wrapf(noAccessErr, "no playbook access; no team view permission for team `%s`", playbook.TeamID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.PlaybookIsPublic(playbook) {
|
|
||||||
if p.hasPermissionsToPlaybook(userID, playbook, model.PermissionPublicPlaybookView) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.hasPermissionsToPlaybook(userID, playbook, model.PermissionPrivatePlaybookView) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return noAccessErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) PlaybookMakePrivate(userID string, playbook Playbook) error {
|
|
||||||
if p.hasPermissionsToPlaybook(userID, playbook, model.PermissionPublicPlaybookMakePrivate) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to make playbook `%s` private", userID, playbook.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) PlaybookMakePublic(userID string, playbook Playbook) error {
|
|
||||||
if p.hasPermissionsToPlaybook(userID, playbook, model.PermissionPrivatePlaybookMakePublic) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to make playbook `%s` public", userID, playbook.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) RunCreate(userID string, playbook Playbook) error {
|
|
||||||
if p.hasPermissionsToPlaybook(userID, playbook, model.PermissionRunCreate) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to run playbook `%s`", userID, playbook.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) RunManageProperties(userID, runID string) error {
|
|
||||||
run, err := p.runService.GetPlaybookRun(runID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "Unable to get run to determine permissions, run id `%s`", runID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.runManagePropertiesWithPlaybookRun(userID, run)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) runManagePropertiesWithPlaybookRun(userID string, run *PlaybookRun) error {
|
|
||||||
if run.OwnerUserID == userID {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, participantID := range run.ParticipantIDs {
|
|
||||||
if participantID == userID {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if IsSystemAdmin(userID, p.api) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to manage run `%s`", userID, run.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) RunView(userID, runID string) error {
|
|
||||||
run, err := p.runService.GetPlaybookRun(runID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "Unable to get run to determine permissions, run id `%s`", runID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Has permission if is the owner of the run
|
|
||||||
if run.OwnerUserID == userID {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Or if is a participant of the run
|
|
||||||
for _, participantID := range run.ParticipantIDs {
|
|
||||||
if participantID == userID {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Or has view access to the playbook that created it
|
|
||||||
return p.PlaybookView(userID, run.PlaybookID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) ChannelActionCreate(userID, channelID string) error {
|
|
||||||
if IsSystemAdmin(userID, p.api) || CanManageChannelProperties(userID, channelID, p.api) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to create actions for channel `%s`", userID, channelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) ChannelActionView(userID, channelID string) error {
|
|
||||||
if p.api.HasPermissionToChannel(userID, channelID, model.PermissionReadChannel) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to view actions for channel `%s`", userID, channelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PermissionsService) ChannelActionUpdate(userID, channelID string) error {
|
|
||||||
if IsSystemAdmin(userID, p.api) || CanManageChannelProperties(userID, channelID, p.api) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to update actions for channel `%s`", userID, channelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSystemAdmin returns true if the userID is a system admin
|
|
||||||
func IsSystemAdmin(userID string, api playbooks.ServicesAPI) bool {
|
|
||||||
return api.HasPermissionTo(userID, model.PermissionManageSystem)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanManageChannelProperties returns true if the userID is allowed to manage the properties of channelID
|
|
||||||
func CanManageChannelProperties(userID, channelID string, api playbooks.ServicesAPI) bool {
|
|
||||||
channel, err := api.GetChannelByID(channelID)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
permission := model.PermissionManagePublicChannelProperties
|
|
||||||
if channel.Type == model.ChannelTypePrivate {
|
|
||||||
permission = model.PermissionManagePrivateChannelProperties
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.HasPermissionToChannel(userID, channelID, permission)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CanPostToChannel(userID, channelID string, api playbooks.ServicesAPI) bool {
|
|
||||||
return api.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost)
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsMemberOfTeam(userID, teamID string, api playbooks.ServicesAPI) bool {
|
|
||||||
teamMember, err := api.GetTeamMember(teamID, userID)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return teamMember.DeleteAt == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequesterInfo holds the userID and teamID that this request is regarding, and permissions
|
|
||||||
// for the user making the request
|
|
||||||
type RequesterInfo struct {
|
|
||||||
UserID string
|
|
||||||
TeamID string
|
|
||||||
IsAdmin bool
|
|
||||||
IsGuest bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsGuest returns true if the userID is a system guest
|
|
||||||
func IsGuest(userID string, api playbooks.ServicesAPI) (bool, error) {
|
|
||||||
user, err := api.GetUserByID(userID)
|
|
||||||
if err != nil {
|
|
||||||
return false, errors.Wrapf(err, "Unable to get user to determine permissions, user id `%s`", userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.IsGuest(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRequesterInfo(userID string, api playbooks.ServicesAPI) (RequesterInfo, error) {
|
|
||||||
isAdmin := IsSystemAdmin(userID, api)
|
|
||||||
|
|
||||||
isGuest, err := IsGuest(userID, api)
|
|
||||||
if err != nil {
|
|
||||||
return RequesterInfo{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return RequesterInfo{
|
|
||||||
UserID: userID,
|
|
||||||
IsAdmin: isAdmin,
|
|
||||||
IsGuest: isGuest,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@@ -1,741 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql/driver"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"gopkg.in/guregu/null.v4"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Playbook represents a desired business outcome, from which playbook runs are started to solve
|
|
||||||
// a specific instance.
|
|
||||||
// The tag export supports the export/import feature. If the field makes sense for export, the value should be
|
|
||||||
// the JSON name of the item in the export format. If the field should not be exported the value should be "-".
|
|
||||||
// Fields should be exported if they are not server specific like InvitedUserIDs or are tracking metadata like CreateAt.
|
|
||||||
type Playbook struct {
|
|
||||||
ID string `json:"id" export:"-"`
|
|
||||||
Title string `json:"title" export:"title"`
|
|
||||||
Description string `json:"description" export:"description"`
|
|
||||||
Public bool `json:"public" export:"-"`
|
|
||||||
TeamID string `json:"team_id" export:"-"`
|
|
||||||
CreatePublicPlaybookRun bool `json:"create_public_playbook_run" export:"-"`
|
|
||||||
CreateAt int64 `json:"create_at" export:"-"`
|
|
||||||
UpdateAt int64 `json:"update_at" export:"-"`
|
|
||||||
DeleteAt int64 `json:"delete_at" export:"-"`
|
|
||||||
NumStages int64 `json:"num_stages" export:"-"`
|
|
||||||
NumSteps int64 `json:"num_steps" export:"-"`
|
|
||||||
NumRuns int64 `json:"num_runs" export:"-"`
|
|
||||||
NumActions int64 `json:"num_actions" export:"-"`
|
|
||||||
LastRunAt int64 `json:"last_run_at" export:"-"`
|
|
||||||
Checklists []Checklist `json:"checklists" export:"-"`
|
|
||||||
Members []PlaybookMember `json:"members" export:"-"`
|
|
||||||
ReminderMessageTemplate string `json:"reminder_message_template" export:"reminder_message_template"`
|
|
||||||
ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds" export:"reminder_timer_default_seconds"`
|
|
||||||
StatusUpdateEnabled bool `json:"status_update_enabled" export:"status_update_enabled"`
|
|
||||||
InvitedUserIDs []string `json:"invited_user_ids" export:"-"`
|
|
||||||
InvitedGroupIDs []string `json:"invited_group_ids" export:"-"`
|
|
||||||
InviteUsersEnabled bool `json:"invite_users_enabled" export:"-"`
|
|
||||||
DefaultOwnerID string `json:"default_owner_id" export:"-"`
|
|
||||||
DefaultOwnerEnabled bool `json:"default_owner_enabled" export:"-"`
|
|
||||||
BroadcastChannelIDs []string `json:"broadcast_channel_ids" export:"-"`
|
|
||||||
WebhookOnCreationURLs []string `json:"webhook_on_creation_urls" export:"-"`
|
|
||||||
WebhookOnCreationEnabled bool `json:"webhook_on_creation_enabled" export:"-"`
|
|
||||||
MessageOnJoin string `json:"message_on_join" export:"message_on_join"`
|
|
||||||
MessageOnJoinEnabled bool `json:"message_on_join_enabled" export:"message_on_join_enabled"`
|
|
||||||
RetrospectiveReminderIntervalSeconds int64 `json:"retrospective_reminder_interval_seconds" export:"retrospective_reminder_interval_seconds"`
|
|
||||||
RetrospectiveTemplate string `json:"retrospective_template" export:"retrospective_template"`
|
|
||||||
RetrospectiveEnabled bool `json:"retrospective_enabled" export:"retrospective_enabled"`
|
|
||||||
WebhookOnStatusUpdateURLs []string `json:"webhook_on_status_update_urls" export:"-"`
|
|
||||||
SignalAnyKeywords []string `json:"signal_any_keywords" export:"signal_any_keywords"`
|
|
||||||
SignalAnyKeywordsEnabled bool `json:"signal_any_keywords_enabled" export:"signal_any_keywords_enabled"`
|
|
||||||
CategorizeChannelEnabled bool `json:"categorize_channel_enabled" export:"categorize_channel_enabled"`
|
|
||||||
CategoryName string `json:"category_name" export:"category_name"`
|
|
||||||
RunSummaryTemplateEnabled bool `json:"run_summary_template_enabled" export:"run_summary_template_enabled"`
|
|
||||||
RunSummaryTemplate string `json:"run_summary_template" export:"run_summary_template"`
|
|
||||||
ChannelNameTemplate string `json:"channel_name_template" export:"channel_name_template"`
|
|
||||||
DefaultPlaybookAdminRole string `json:"default_playbook_admin_role" export:"-"`
|
|
||||||
DefaultPlaybookMemberRole string `json:"default_playbook_member_role" export:"-"`
|
|
||||||
DefaultRunAdminRole string `json:"default_run_admin_role" export:"-"`
|
|
||||||
DefaultRunMemberRole string `json:"default_run_member_role" export:"-"`
|
|
||||||
Metrics []PlaybookMetricConfig `json:"metrics" export:"metrics"`
|
|
||||||
ActiveRuns int64 `json:"active_runs" export:"-"`
|
|
||||||
CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant" export:"create_channel_member_on_new_participant"`
|
|
||||||
RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant" export:"create_channel_member_on_removed_participant"`
|
|
||||||
|
|
||||||
// ChannelID is the identifier of the channel that would be -potentially- linked
|
|
||||||
// to any new run of this playbook
|
|
||||||
ChannelID string `json:"channel_id" export:"channel_id"`
|
|
||||||
|
|
||||||
// ChannelMode is the playbook>run>channel flow used
|
|
||||||
ChannelMode ChannelPlaybookMode `json:"channel_mode" export:"channel_mode"`
|
|
||||||
|
|
||||||
// Deprecated: preserved for backwards compatibility with v1.27
|
|
||||||
BroadcastEnabled bool `json:"broadcast_enabled" export:"-"`
|
|
||||||
WebhookOnStatusUpdateEnabled bool `json:"webhook_on_status_update_enabled" export:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
PlaybookRoleMember = "playbook_member"
|
|
||||||
PlaybookRoleAdmin = "playbook_admin"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
MetricTypeDuration = "metric_duration"
|
|
||||||
MetricTypeCurrency = "metric_currency"
|
|
||||||
MetricTypeInteger = "metric_integer"
|
|
||||||
)
|
|
||||||
|
|
||||||
const MaxMetricsPerPlaybook = 4
|
|
||||||
|
|
||||||
type PlaybookMember struct {
|
|
||||||
UserID string `json:"user_id"`
|
|
||||||
Roles []string `json:"roles"`
|
|
||||||
SchemeRoles []string `json:"scheme_roles"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlaybookMetricConfig struct {
|
|
||||||
ID string `json:"id" export:"-"`
|
|
||||||
PlaybookID string `json:"playbook_id" export:"-"`
|
|
||||||
Title string `json:"title" export:"title"`
|
|
||||||
Description string `json:"description" export:"description"`
|
|
||||||
Type string `json:"type" export:"type"`
|
|
||||||
Target null.Int `json:"target" export:"target"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm PlaybookMember) Clone() PlaybookMember {
|
|
||||||
newPlaybookMember := pm
|
|
||||||
if len(pm.Roles) != 0 {
|
|
||||||
newPlaybookMember.Roles = append([]string(nil), pm.Roles...)
|
|
||||||
}
|
|
||||||
if len(pm.SchemeRoles) != 0 {
|
|
||||||
newPlaybookMember.SchemeRoles = append([]string(nil), pm.SchemeRoles...)
|
|
||||||
}
|
|
||||||
return newPlaybookMember
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Playbook) Clone() Playbook {
|
|
||||||
newPlaybook := p
|
|
||||||
var newChecklists []Checklist
|
|
||||||
for _, c := range p.Checklists {
|
|
||||||
newChecklists = append(newChecklists, c.Clone())
|
|
||||||
}
|
|
||||||
newPlaybook.Checklists = newChecklists
|
|
||||||
newPlaybook.Metrics = append([]PlaybookMetricConfig(nil), p.Metrics...)
|
|
||||||
var newMembers []PlaybookMember
|
|
||||||
for _, m := range p.Members {
|
|
||||||
newMembers = append(newMembers, m.Clone())
|
|
||||||
}
|
|
||||||
newPlaybook.Members = newMembers
|
|
||||||
if len(p.InvitedUserIDs) != 0 {
|
|
||||||
newPlaybook.InvitedUserIDs = append([]string(nil), p.InvitedUserIDs...)
|
|
||||||
}
|
|
||||||
if len(p.InvitedGroupIDs) != 0 {
|
|
||||||
newPlaybook.InvitedGroupIDs = append([]string(nil), p.InvitedGroupIDs...)
|
|
||||||
}
|
|
||||||
if len(p.SignalAnyKeywords) != 0 {
|
|
||||||
newPlaybook.SignalAnyKeywords = append([]string(nil), p.SignalAnyKeywords...)
|
|
||||||
}
|
|
||||||
if len(p.BroadcastChannelIDs) != 0 {
|
|
||||||
newPlaybook.BroadcastChannelIDs = append([]string(nil), p.BroadcastChannelIDs...)
|
|
||||||
}
|
|
||||||
if len(p.WebhookOnCreationURLs) != 0 {
|
|
||||||
newPlaybook.WebhookOnCreationURLs = append([]string(nil), p.WebhookOnCreationURLs...)
|
|
||||||
}
|
|
||||||
if len(p.WebhookOnStatusUpdateURLs) != 0 {
|
|
||||||
newPlaybook.WebhookOnStatusUpdateURLs = append([]string(nil), p.WebhookOnStatusUpdateURLs...)
|
|
||||||
}
|
|
||||||
return newPlaybook
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Playbook) MarshalJSON() ([]byte, error) {
|
|
||||||
type Alias Playbook
|
|
||||||
|
|
||||||
old := Alias(p.Clone())
|
|
||||||
// replace nils with empty slices for the frontend
|
|
||||||
if old.Checklists == nil {
|
|
||||||
old.Checklists = []Checklist{}
|
|
||||||
}
|
|
||||||
for j, cl := range old.Checklists {
|
|
||||||
if cl.Items == nil {
|
|
||||||
old.Checklists[j].Items = []ChecklistItem{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if old.Members == nil {
|
|
||||||
old.Members = []PlaybookMember{}
|
|
||||||
}
|
|
||||||
if old.Metrics == nil {
|
|
||||||
old.Metrics = []PlaybookMetricConfig{}
|
|
||||||
}
|
|
||||||
if old.InvitedUserIDs == nil {
|
|
||||||
old.InvitedUserIDs = []string{}
|
|
||||||
}
|
|
||||||
if old.InvitedGroupIDs == nil {
|
|
||||||
old.InvitedGroupIDs = []string{}
|
|
||||||
}
|
|
||||||
if old.SignalAnyKeywords == nil {
|
|
||||||
old.SignalAnyKeywords = []string{}
|
|
||||||
}
|
|
||||||
if old.BroadcastChannelIDs == nil {
|
|
||||||
old.BroadcastChannelIDs = []string{}
|
|
||||||
}
|
|
||||||
if old.WebhookOnCreationURLs == nil {
|
|
||||||
old.WebhookOnCreationURLs = []string{}
|
|
||||||
}
|
|
||||||
if old.WebhookOnStatusUpdateURLs == nil {
|
|
||||||
old.WebhookOnStatusUpdateURLs = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(old)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Playbook) GetRunChannelID() string {
|
|
||||||
if p.ChannelMode == PlaybookRunLinkExistingChannel {
|
|
||||||
return p.ChannelID
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChecklistCommon allows access on common fields of Checklist and api.UpdateChecklist
|
|
||||||
type ChecklistCommon interface {
|
|
||||||
GetItems() []ChecklistItemCommon
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checklist represents a checklist in a playbook.
|
|
||||||
type Checklist struct {
|
|
||||||
// ID is the identifier of the checklist.
|
|
||||||
ID string `json:"id" export:"-"`
|
|
||||||
|
|
||||||
// Title is the name of the checklist.
|
|
||||||
Title string `json:"title" export:"title"`
|
|
||||||
|
|
||||||
// Items is an array of all the items in the checklist.
|
|
||||||
Items []ChecklistItem `json:"items" export:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Checklist) GetItems() []ChecklistItemCommon {
|
|
||||||
items := make([]ChecklistItemCommon, len(c.Items))
|
|
||||||
for i := range c.Items {
|
|
||||||
items[i] = &c.Items[i]
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Checklist) Clone() Checklist {
|
|
||||||
newChecklist := c
|
|
||||||
newChecklist.Items = append([]ChecklistItem(nil), c.Items...)
|
|
||||||
return newChecklist
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChecklistItemCommon allows access on common fields of ChecklistItem and api.UpdateChecklistItem
|
|
||||||
type ChecklistItemCommon interface {
|
|
||||||
GetAssigneeID() string
|
|
||||||
|
|
||||||
SetAssigneeModified(modified int64)
|
|
||||||
SetState(state string)
|
|
||||||
SetStateModified(modified int64)
|
|
||||||
SetCommandLastRun(lastRun int64)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChecklistItem represents an item in a checklist.
|
|
||||||
type ChecklistItem struct {
|
|
||||||
// ID is the identifier of the checklist item.
|
|
||||||
ID string `json:"id" export:"-"`
|
|
||||||
|
|
||||||
// Title is the content of the checklist item.
|
|
||||||
Title string `json:"title" export:"title"`
|
|
||||||
|
|
||||||
// State is the state of the checklist item: "closed" if it's checked, "skipped" if it has
|
|
||||||
// been skipped, the empty string otherwise.
|
|
||||||
State string `json:"state" export:"-"`
|
|
||||||
|
|
||||||
// StateModified is the timestamp, in milliseconds since epoch, of the last time the item's
|
|
||||||
// state was modified. 0 if it was never modified.
|
|
||||||
StateModified int64 `json:"state_modified" export:"-"`
|
|
||||||
|
|
||||||
// AssigneeID is the identifier of the user to whom this item is assigned.
|
|
||||||
AssigneeID string `json:"assignee_id" export:"-"`
|
|
||||||
|
|
||||||
// AssigneeModified is the timestamp, in milliseconds since epoch, of the last time the item's
|
|
||||||
// assignee was modified. 0 if it was never modified.
|
|
||||||
AssigneeModified int64 `json:"assignee_modified" export:"-"`
|
|
||||||
|
|
||||||
// Command, if not empty, is the slash command that can be run as part of this item.
|
|
||||||
Command string `json:"command" export:"command"`
|
|
||||||
|
|
||||||
// CommandLastRun is the timestamp, in milliseconds since epoch, of the last time the item's
|
|
||||||
// slash command was run. 0 if it was never run.
|
|
||||||
CommandLastRun int64 `json:"command_last_run" export:"-"`
|
|
||||||
|
|
||||||
// Description is a string with the markdown content of the long description of the item.
|
|
||||||
Description string `json:"description" export:"description"`
|
|
||||||
|
|
||||||
// LastSkipped is the timestamp, in milliseconds since epoch, of the last time the item
|
|
||||||
// was skipped. 0 if it was never skipped.
|
|
||||||
LastSkipped int64 `json:"delete_at" export:"-"`
|
|
||||||
|
|
||||||
// DueDate is the timestamp, in milliseconds since epoch. indicates relative or absolute due date
|
|
||||||
// of the checklist item. 0 if not set.
|
|
||||||
// Playbook can have only relative timstamp, run can have only absolute timestamp.
|
|
||||||
DueDate int64 `json:"due_date" export:"due_date"`
|
|
||||||
|
|
||||||
// TaskActions is an array of all the task actions associated with this task.
|
|
||||||
TaskActions []TaskAction `json:"task_actions" export:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *ChecklistItem) GetAssigneeID() string {
|
|
||||||
return ci.AssigneeID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *ChecklistItem) SetAssigneeModified(modified int64) {
|
|
||||||
ci.AssigneeModified = modified
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *ChecklistItem) SetState(state string) {
|
|
||||||
ci.State = state
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *ChecklistItem) SetStateModified(modified int64) {
|
|
||||||
ci.StateModified = modified
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ci *ChecklistItem) SetCommandLastRun(lastRun int64) {
|
|
||||||
ci.CommandLastRun = lastRun
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetPlaybooksResults struct {
|
|
||||||
TotalCount int `json:"total_count"`
|
|
||||||
PageCount int `json:"page_count"`
|
|
||||||
HasMore bool `json:"has_more"`
|
|
||||||
Items []Playbook `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON customizes the JSON marshalling for GetPlaybooksResults by rendering a nil Items as
|
|
||||||
// an empty slice instead.
|
|
||||||
func (r GetPlaybooksResults) MarshalJSON() ([]byte, error) {
|
|
||||||
type Alias GetPlaybooksResults
|
|
||||||
|
|
||||||
if r.Items == nil {
|
|
||||||
r.Items = []Playbook{}
|
|
||||||
}
|
|
||||||
|
|
||||||
aux := &struct {
|
|
||||||
*Alias
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(&r),
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(aux)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaybookService is the playbook service for managing playbooks
|
|
||||||
// userID is the user initiating the event.
|
|
||||||
type PlaybookService interface {
|
|
||||||
// Get retrieves a playbook. Returns ErrNotFound if not found.
|
|
||||||
Get(id string) (Playbook, error)
|
|
||||||
|
|
||||||
// Create creates a new playbook
|
|
||||||
Create(playbook Playbook, userID string) (string, error)
|
|
||||||
|
|
||||||
// Import imports a new playbook
|
|
||||||
Import(playbook Playbook, userID string) (string, error)
|
|
||||||
|
|
||||||
// GetPlaybooks retrieves all playbooks
|
|
||||||
GetPlaybooks() ([]Playbook, error)
|
|
||||||
|
|
||||||
// GetPlaybooksForTeam retrieves all playbooks on the specified team given the provided options
|
|
||||||
GetPlaybooksForTeam(requesterInfo RequesterInfo, teamID string, opts PlaybookFilterOptions) (GetPlaybooksResults, error)
|
|
||||||
|
|
||||||
// Update updates a playbook
|
|
||||||
Update(playbook Playbook, userID string) error
|
|
||||||
|
|
||||||
// Archive archives a playbook
|
|
||||||
Archive(playbook Playbook, userID string) error
|
|
||||||
|
|
||||||
// Restores an archived playbook
|
|
||||||
Restore(playbook Playbook, userID string) error
|
|
||||||
|
|
||||||
// AutoFollow method lets user auto-follow all runs of a specific playbook
|
|
||||||
AutoFollow(playbookID, userID string) error
|
|
||||||
|
|
||||||
// AutoUnfollow method lets user to not auto-follow the newly created playbook runs
|
|
||||||
AutoUnfollow(playbookID, userID string) error
|
|
||||||
|
|
||||||
// GetAutoFollows returns list of users who auto-follows a playbook
|
|
||||||
GetAutoFollows(playbookID string) ([]string, error)
|
|
||||||
|
|
||||||
// Duplicate duplicates a playbook
|
|
||||||
Duplicate(playbook Playbook, userID string) (string, error)
|
|
||||||
|
|
||||||
// Get top playbooks for teams
|
|
||||||
GetTopPlaybooksForTeam(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error)
|
|
||||||
|
|
||||||
// Get top playbooks for users
|
|
||||||
GetTopPlaybooksForUser(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaybookStore is an interface for storing playbooks
|
|
||||||
type PlaybookStore interface {
|
|
||||||
// Get retrieves a playbook
|
|
||||||
Get(id string) (Playbook, error)
|
|
||||||
|
|
||||||
// Create creates a new playbook
|
|
||||||
Create(playbook Playbook) (string, error)
|
|
||||||
|
|
||||||
// GetPlaybooks retrieves all playbooks
|
|
||||||
GetPlaybooks() ([]Playbook, error)
|
|
||||||
|
|
||||||
// GetPlaybooksForTeam retrieves all playbooks on the specified team
|
|
||||||
GetPlaybooksForTeam(requesterInfo RequesterInfo, teamID string, opts PlaybookFilterOptions) (GetPlaybooksResults, error)
|
|
||||||
|
|
||||||
// GetPlaybooksWithKeywords retrieves all playbooks with keywords enabled
|
|
||||||
GetPlaybooksWithKeywords(opts PlaybookFilterOptions) ([]Playbook, error)
|
|
||||||
|
|
||||||
// GetTimeLastUpdated retrieves time last playbook was updated at.
|
|
||||||
// Passed argument determines whether to include playbooks with
|
|
||||||
// SignalAnyKeywordsEnabled flag or not.
|
|
||||||
GetTimeLastUpdated(onlyPlaybooksWithKeywordsEnabled bool) (int64, error)
|
|
||||||
|
|
||||||
// GetPlaybookIDsForUser retrieves playbooks user can access
|
|
||||||
GetPlaybookIDsForUser(userID, teamID string) ([]string, error)
|
|
||||||
|
|
||||||
// Update updates a playbook
|
|
||||||
Update(playbook Playbook) error
|
|
||||||
|
|
||||||
// GraphqlUpdate taking a setmap for graphql
|
|
||||||
GraphqlUpdate(id string, setmap map[string]interface{}) error
|
|
||||||
|
|
||||||
// Archive archives a playbook
|
|
||||||
Archive(id string) error
|
|
||||||
|
|
||||||
// Restore restores a deleted playbook
|
|
||||||
Restore(id string) error
|
|
||||||
|
|
||||||
// AutoFollow method lets user auto-follow all runs of a specific playbook
|
|
||||||
AutoFollow(playbookID, userID string) error
|
|
||||||
|
|
||||||
// AutoUnfollow method lets user to not auto-follow the newly created playbook runs
|
|
||||||
AutoUnfollow(playbookID, userID string) error
|
|
||||||
|
|
||||||
// GetAutoFollows returns list of users who auto-follows a playbook
|
|
||||||
GetAutoFollows(playbookID string) ([]string, error)
|
|
||||||
|
|
||||||
// GetPlaybooksActiveTotal returns number of active playbooks
|
|
||||||
GetPlaybooksActiveTotal() (int64, error)
|
|
||||||
|
|
||||||
// GetMetric retrieves a metric by ID
|
|
||||||
GetMetric(id string) (*PlaybookMetricConfig, error)
|
|
||||||
|
|
||||||
// AddMetric adds a metric
|
|
||||||
AddMetric(playbookID string, config PlaybookMetricConfig) error
|
|
||||||
|
|
||||||
// UpdateMetric updates a metric
|
|
||||||
UpdateMetric(id string, setmap map[string]interface{}) error
|
|
||||||
|
|
||||||
// DeleteMetric deletes a metric
|
|
||||||
DeleteMetric(id string) error
|
|
||||||
|
|
||||||
// Get top playbooks for teams
|
|
||||||
GetTopPlaybooksForTeam(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error)
|
|
||||||
|
|
||||||
// Get top playbooks for users
|
|
||||||
GetTopPlaybooksForUser(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error)
|
|
||||||
|
|
||||||
// AddPlaybookMember adds a user as a member to a playbook
|
|
||||||
AddPlaybookMember(id string, memberID string) error
|
|
||||||
|
|
||||||
// RemovePlaybookMember removes a user from a playbook
|
|
||||||
RemovePlaybookMember(id string, memberID string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaybookTelemetry defines the methods that the Playbook service needs from the RudderTelemetry.
|
|
||||||
// userID is the user initiating the event.
|
|
||||||
type PlaybookTelemetry interface {
|
|
||||||
// CreatePlaybook tracks the creation of a playbook.
|
|
||||||
CreatePlaybook(playbook Playbook, userID string)
|
|
||||||
|
|
||||||
// ImportPlaybook tracks the import of a playbook.
|
|
||||||
ImportPlaybook(playbook Playbook, userID string)
|
|
||||||
|
|
||||||
// UpdatePlaybook tracks the update of a playbook.
|
|
||||||
UpdatePlaybook(playbook Playbook, userID string)
|
|
||||||
|
|
||||||
// DeletePlaybook tracks the deletion of a playbook.
|
|
||||||
DeletePlaybook(playbook Playbook, userID string)
|
|
||||||
|
|
||||||
// RestorePlaybook tracks the restoration of a playbook.
|
|
||||||
RestorePlaybook(playbook Playbook, userID string)
|
|
||||||
|
|
||||||
// FrontendTelemetryForPlaybook tracks an event originating from the frontend
|
|
||||||
FrontendTelemetryForPlaybook(playbook Playbook, userID, action string)
|
|
||||||
|
|
||||||
// FrontendTelemetryForPlaybookTemplate tracks an event originating from the frontend
|
|
||||||
FrontendTelemetryForPlaybookTemplate(templateName string, userID, action string)
|
|
||||||
|
|
||||||
// AutoFollowPlaybook tracks the auto-follow of a playbook.
|
|
||||||
AutoFollowPlaybook(playbook Playbook, userID string)
|
|
||||||
|
|
||||||
// AutoUnfollowPlaybook tracks the auto-unfollow of a playbook.
|
|
||||||
AutoUnfollowPlaybook(playbook Playbook, userID string)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ChecklistItemStateOpen = ""
|
|
||||||
ChecklistItemStateInProgress = "in_progress"
|
|
||||||
ChecklistItemStateClosed = "closed"
|
|
||||||
ChecklistItemStateSkipped = "skipped"
|
|
||||||
)
|
|
||||||
|
|
||||||
func IsValidChecklistItemState(state string) bool {
|
|
||||||
return state == ChecklistItemStateClosed ||
|
|
||||||
state == ChecklistItemStateInProgress ||
|
|
||||||
state == ChecklistItemStateOpen ||
|
|
||||||
state == ChecklistItemStateSkipped
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsValidChecklistItemIndex(checklists []Checklist, checklistNum, itemNum int) bool {
|
|
||||||
return checklists != nil && checklistNum >= 0 && itemNum >= 0 && checklistNum < len(checklists) && itemNum < len(checklists[checklistNum].Items)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaybookFilterOptions specifies the parameters when getting playbooks.
|
|
||||||
type PlaybookFilterOptions struct {
|
|
||||||
Sort SortField
|
|
||||||
Direction SortDirection
|
|
||||||
SearchTerm string
|
|
||||||
WithArchived bool
|
|
||||||
WithMembershipOnly bool //if true will return only playbooks you are a member of
|
|
||||||
PlaybookIDs []string
|
|
||||||
|
|
||||||
// Pagination options.
|
|
||||||
Page int
|
|
||||||
PerPage int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone duplicates the given options.
|
|
||||||
func (o *PlaybookFilterOptions) Clone() PlaybookFilterOptions {
|
|
||||||
return *o
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate returns a new, validated filter options or returns an error if invalid.
|
|
||||||
func (o PlaybookFilterOptions) Validate() (PlaybookFilterOptions, error) {
|
|
||||||
options := o.Clone()
|
|
||||||
|
|
||||||
if options.PerPage <= 0 {
|
|
||||||
options.PerPage = PerPageDefault
|
|
||||||
}
|
|
||||||
|
|
||||||
options.Sort = SortField(strings.ToLower(string(options.Sort)))
|
|
||||||
switch options.Sort {
|
|
||||||
case SortByID:
|
|
||||||
case SortByTitle:
|
|
||||||
case SortByStages:
|
|
||||||
case SortBySteps:
|
|
||||||
case "": // default
|
|
||||||
options.Sort = SortByID
|
|
||||||
default:
|
|
||||||
return PlaybookFilterOptions{}, errors.Errorf("unsupported sort '%s'", options.Sort)
|
|
||||||
}
|
|
||||||
|
|
||||||
options.Direction = SortDirection(strings.ToUpper(string(options.Direction)))
|
|
||||||
switch options.Direction {
|
|
||||||
case DirectionAsc:
|
|
||||||
case DirectionDesc:
|
|
||||||
case "": //default
|
|
||||||
options.Direction = DirectionAsc
|
|
||||||
default:
|
|
||||||
return PlaybookFilterOptions{}, errors.Errorf("unsupported direction '%s'", options.Direction)
|
|
||||||
}
|
|
||||||
|
|
||||||
return options, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateWebhookURLs(urls []string) error {
|
|
||||||
if len(urls) > 64 {
|
|
||||||
return errors.New("too many registered urls, limit to less than 64")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, webhook := range urls {
|
|
||||||
reqURL, err := url.ParseRequestURI(webhook)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "unable to parse webhook: %v", webhook)
|
|
||||||
}
|
|
||||||
|
|
||||||
if reqURL.Scheme != "http" && reqURL.Scheme != "https" {
|
|
||||||
return fmt.Errorf("protocol in webhook URL is %s; only HTTP and HTTPS are accepted", reqURL.Scheme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateCategoryName(categoryName string) error {
|
|
||||||
categoryNameLength := len(categoryName)
|
|
||||||
if categoryNameLength > 22 {
|
|
||||||
msg := fmt.Sprintf("invalid category name: %s (maximum length is 22 characters)", categoryName)
|
|
||||||
return errors.Errorf(msg)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CleanUpChecklists sets empty values for checklist fields that are not editable
|
|
||||||
func CleanUpChecklists[T ChecklistCommon](checklists []T) {
|
|
||||||
for listIndex := range checklists {
|
|
||||||
items := checklists[listIndex].GetItems()
|
|
||||||
for itemIndex := range items {
|
|
||||||
items[itemIndex].SetAssigneeModified(0)
|
|
||||||
items[itemIndex].SetState("")
|
|
||||||
items[itemIndex].SetStateModified(0)
|
|
||||||
items[itemIndex].SetCommandLastRun(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidatePreAssignment checks if invitations are enabled and if all assignees are also invited
|
|
||||||
func ValidatePreAssignment(assignees []string, invitedUsers []string, inviteUsersEnabled bool) error {
|
|
||||||
if len(assignees) > 0 && !inviteUsersEnabled {
|
|
||||||
return errors.New("invitations are disabled")
|
|
||||||
}
|
|
||||||
if !assigneesAreInvited(assignees, invitedUsers) {
|
|
||||||
return errors.New("users missing in invite user list")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDistinctAssignees returns a list of distinct user ids that are assignees in the given checklists
|
|
||||||
func GetDistinctAssignees[T ChecklistCommon](checklists []T) []string {
|
|
||||||
uMap := make(map[string]bool)
|
|
||||||
for _, cl := range checklists {
|
|
||||||
for _, ci := range cl.GetItems() {
|
|
||||||
if id := ci.GetAssigneeID(); id != "" && !uMap[id] {
|
|
||||||
uMap[id] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uIds := make([]string, 0, len(uMap))
|
|
||||||
for k := range uMap {
|
|
||||||
uIds = append(uIds, k)
|
|
||||||
}
|
|
||||||
return uIds
|
|
||||||
}
|
|
||||||
|
|
||||||
func assigneesAreInvited(assignees []string, invited []string) bool {
|
|
||||||
for _, assignee := range assignees {
|
|
||||||
found := false
|
|
||||||
for _, user := range invited {
|
|
||||||
if user == assignee {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeDuplicates(a []string) []string {
|
|
||||||
items := make(map[string]bool)
|
|
||||||
for _, item := range a {
|
|
||||||
if item != "" {
|
|
||||||
items[item] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res := make([]string, 0, len(items))
|
|
||||||
for item := range items {
|
|
||||||
res = append(res, item)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func ProcessSignalAnyKeywords(keywords []string) []string {
|
|
||||||
return removeDuplicates(keywords)
|
|
||||||
}
|
|
||||||
|
|
||||||
// models for playbooks-insights
|
|
||||||
|
|
||||||
// PlaybooksInsightsList is a response type with pagination support.
|
|
||||||
type PlaybooksInsightsList struct {
|
|
||||||
HasNext bool `json:"has_next"`
|
|
||||||
Items []*PlaybookInsight `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaybookInsight gives insight into activities related to a playbook
|
|
||||||
|
|
||||||
type PlaybookInsight struct {
|
|
||||||
// ID of the playbook
|
|
||||||
// required: true
|
|
||||||
PlaybookID string `json:"playbook_id"`
|
|
||||||
|
|
||||||
// Run count of playbook
|
|
||||||
// required: true
|
|
||||||
NumRuns int `json:"num_runs"`
|
|
||||||
|
|
||||||
// Title of playbook
|
|
||||||
// required: true
|
|
||||||
Title string `json:"title"`
|
|
||||||
|
|
||||||
// Time the playbook was last run.
|
|
||||||
// required: false
|
|
||||||
LastRunAt int64 `json:"last_run_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChannelPlaybookMode is a type alias to hold all possible
|
|
||||||
// modes for playbook > run > channel relation
|
|
||||||
type ChannelPlaybookMode int
|
|
||||||
|
|
||||||
const (
|
|
||||||
PlaybookRunCreateNewChannel ChannelPlaybookMode = iota
|
|
||||||
PlaybookRunLinkExistingChannel
|
|
||||||
)
|
|
||||||
|
|
||||||
var channelPlaybookTypes = [...]string{
|
|
||||||
PlaybookRunCreateNewChannel: "create_new_channel",
|
|
||||||
PlaybookRunLinkExistingChannel: "link_existing_channel",
|
|
||||||
}
|
|
||||||
|
|
||||||
// String creates the string version of the TelemetryTrack
|
|
||||||
func (cpm ChannelPlaybookMode) String() string {
|
|
||||||
return channelPlaybookTypes[cpm]
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalText converts a ChannelPlaybookMode to a string for serializers (including JSON)
|
|
||||||
func (cpm ChannelPlaybookMode) MarshalText() ([]byte, error) {
|
|
||||||
return []byte(channelPlaybookTypes[cpm]), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalText parses a ChannelPlaybookMode from text. For deserializers (including JSON)
|
|
||||||
func (cpm *ChannelPlaybookMode) UnmarshalText(text []byte) error {
|
|
||||||
for i, st := range channelPlaybookTypes {
|
|
||||||
if st == string(text) {
|
|
||||||
*cpm = ChannelPlaybookMode(i)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unknown ChannelPlaybookMode: %s", string(text))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan parses a ChannelPlaybookMode back from the DB
|
|
||||||
func (cpm *ChannelPlaybookMode) Scan(src interface{}) error {
|
|
||||||
txt, ok := src.([]byte) // mysql
|
|
||||||
if !ok {
|
|
||||||
txt, ok := src.(string) //postgres
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("could not cast to string: %v", src)
|
|
||||||
}
|
|
||||||
return cpm.UnmarshalText([]byte(txt))
|
|
||||||
}
|
|
||||||
return cpm.UnmarshalText(txt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value represents a ChannelPlaybookMode as a type writable into the DB
|
|
||||||
func (cpm ChannelPlaybookMode) Value() (driver.Value, error) {
|
|
||||||
return cpm.MarshalText()
|
|
||||||
}
|
|
||||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,219 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPlaybookRun_MarshalJSON(t *testing.T) {
|
|
||||||
t.Run("marshal pointer", func(t *testing.T) {
|
|
||||||
testPlaybookRun := &PlaybookRun{}
|
|
||||||
result, err := json.Marshal(testPlaybookRun)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotContains(t, string(result), "null", "update MarshalJSON to initialize nil slices")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("marshal value", func(t *testing.T) {
|
|
||||||
testPlaybookRun := PlaybookRun{}
|
|
||||||
result, err := json.Marshal(testPlaybookRun)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotContains(t, string(result), "null", "update MarshalJSON to initialize nil slices")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlaybookRunFilterOptions_Clone(t *testing.T) {
|
|
||||||
options := PlaybookRunFilterOptions{
|
|
||||||
TeamID: "team_id",
|
|
||||||
Page: 1,
|
|
||||||
PerPage: 10,
|
|
||||||
Sort: SortByID,
|
|
||||||
Direction: DirectionAsc,
|
|
||||||
Statuses: []string{"InProgress", "Finished"},
|
|
||||||
OwnerID: "owner_id",
|
|
||||||
ParticipantID: "participant_id",
|
|
||||||
SearchTerm: "search_term",
|
|
||||||
PlaybookID: "playbook_id",
|
|
||||||
}
|
|
||||||
marshalledOptions, err := json.Marshal(options)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
clone := options.Clone()
|
|
||||||
clone.TeamID = "team_id_clone"
|
|
||||||
clone.Page = 2
|
|
||||||
clone.PerPage = 20
|
|
||||||
clone.Sort = SortByName
|
|
||||||
clone.Direction = DirectionDesc
|
|
||||||
clone.Statuses[0] = "Finished"
|
|
||||||
clone.OwnerID = "owner_id_clone"
|
|
||||||
clone.ParticipantID = "participant_id_clone"
|
|
||||||
clone.SearchTerm = "search_term_clone"
|
|
||||||
clone.PlaybookID = "playbook_id_clone"
|
|
||||||
|
|
||||||
var unmarshalledOptions PlaybookRunFilterOptions
|
|
||||||
err = json.Unmarshal(marshalledOptions, &unmarshalledOptions)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, options, unmarshalledOptions)
|
|
||||||
require.NotEqual(t, clone, unmarshalledOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlaybookRunFilterOptions_Validate(t *testing.T) {
|
|
||||||
t.Run("non-positive PerPage", func(t *testing.T) {
|
|
||||||
options := PlaybookRunFilterOptions{
|
|
||||||
TeamID: model.NewId(),
|
|
||||||
PerPage: -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
validOptions, err := options.Validate()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, options.TeamID, validOptions.TeamID)
|
|
||||||
require.Equal(t, PerPageDefault, validOptions.PerPage)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid sort option", func(t *testing.T) {
|
|
||||||
options := PlaybookRunFilterOptions{
|
|
||||||
TeamID: model.NewId(),
|
|
||||||
Sort: SortField("invalid"),
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := options.Validate()
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid, but wrong case sort option", func(t *testing.T) {
|
|
||||||
options := PlaybookRunFilterOptions{
|
|
||||||
TeamID: model.NewId(),
|
|
||||||
Sort: SortField("END_at"),
|
|
||||||
}
|
|
||||||
|
|
||||||
validOptions, err := options.Validate()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, options.TeamID, validOptions.TeamID)
|
|
||||||
require.Equal(t, SortByEndAt, validOptions.Sort)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid, no explicit sort option", func(t *testing.T) {
|
|
||||||
options := PlaybookRunFilterOptions{
|
|
||||||
TeamID: model.NewId(),
|
|
||||||
}
|
|
||||||
|
|
||||||
validOptions, err := options.Validate()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, options.TeamID, validOptions.TeamID)
|
|
||||||
require.Equal(t, SortByCreateAt, validOptions.Sort)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid sort direction", func(t *testing.T) {
|
|
||||||
options := PlaybookRunFilterOptions{
|
|
||||||
TeamID: model.NewId(),
|
|
||||||
Direction: SortDirection("invalid"),
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := options.Validate()
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid, but wrong case direction option", func(t *testing.T) {
|
|
||||||
options := PlaybookRunFilterOptions{
|
|
||||||
TeamID: model.NewId(),
|
|
||||||
Direction: SortDirection("DEsC"),
|
|
||||||
}
|
|
||||||
|
|
||||||
validOptions, err := options.Validate()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, options.TeamID, validOptions.TeamID)
|
|
||||||
require.Equal(t, DirectionDesc, validOptions.Direction)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid, no explicit direction", func(t *testing.T) {
|
|
||||||
options := PlaybookRunFilterOptions{
|
|
||||||
TeamID: model.NewId(),
|
|
||||||
}
|
|
||||||
|
|
||||||
validOptions, err := options.Validate()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, options.TeamID, validOptions.TeamID)
|
|
||||||
require.Equal(t, DirectionAsc, validOptions.Direction)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid team id", func(t *testing.T) {
|
|
||||||
options := PlaybookRunFilterOptions{
|
|
||||||
TeamID: "invalid",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := options.Validate()
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid owner id", func(t *testing.T) {
|
|
||||||
options := PlaybookRunFilterOptions{
|
|
||||||
TeamID: model.NewId(),
|
|
||||||
OwnerID: "invalid",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := options.Validate()
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid participant id", func(t *testing.T) {
|
|
||||||
options := PlaybookRunFilterOptions{
|
|
||||||
TeamID: model.NewId(),
|
|
||||||
ParticipantID: "invalid",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := options.Validate()
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid playbook id", func(t *testing.T) {
|
|
||||||
options := PlaybookRunFilterOptions{
|
|
||||||
TeamID: model.NewId(),
|
|
||||||
PlaybookID: "invalid",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := options.Validate()
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid statuses", func(t *testing.T) {
|
|
||||||
options := PlaybookRunFilterOptions{
|
|
||||||
TeamID: model.NewId(),
|
|
||||||
Page: 1,
|
|
||||||
PerPage: 10,
|
|
||||||
Sort: SortByID,
|
|
||||||
Direction: DirectionAsc,
|
|
||||||
Statuses: []string{"active", "Finished"},
|
|
||||||
OwnerID: model.NewId(),
|
|
||||||
ParticipantID: model.NewId(),
|
|
||||||
SearchTerm: "search_term",
|
|
||||||
PlaybookID: model.NewId(),
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := options.Validate()
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid status", func(t *testing.T) {
|
|
||||||
options := PlaybookRunFilterOptions{
|
|
||||||
TeamID: model.NewId(),
|
|
||||||
Page: 1,
|
|
||||||
PerPage: 10,
|
|
||||||
Sort: SortByID,
|
|
||||||
Direction: DirectionAsc,
|
|
||||||
Statuses: []string{"InProgress", "Finished"},
|
|
||||||
OwnerID: model.NewId(),
|
|
||||||
ParticipantID: model.NewId(),
|
|
||||||
SearchTerm: "search_term",
|
|
||||||
PlaybookID: model.NewId(),
|
|
||||||
}
|
|
||||||
|
|
||||||
validOptions, err := options.Validate()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, options, validOptions)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/bot"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/metrics"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
playbookCreatedWSEvent = "playbook_created"
|
|
||||||
playbookArchivedWSEvent = "playbook_archived"
|
|
||||||
playbookRestoredWSEvent = "playbook_restored"
|
|
||||||
)
|
|
||||||
|
|
||||||
type playbookService struct {
|
|
||||||
store PlaybookStore
|
|
||||||
poster bot.Poster
|
|
||||||
telemetry PlaybookTelemetry
|
|
||||||
api playbooks.ServicesAPI
|
|
||||||
metricsService *metrics.Metrics
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPlaybookService returns a new playbook service
|
|
||||||
func NewPlaybookService(store PlaybookStore, poster bot.Poster, telemetry PlaybookTelemetry, api playbooks.ServicesAPI, metricsService *metrics.Metrics) PlaybookService {
|
|
||||||
return &playbookService{
|
|
||||||
store: store,
|
|
||||||
poster: poster,
|
|
||||||
telemetry: telemetry,
|
|
||||||
api: api,
|
|
||||||
metricsService: metricsService,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *playbookService) Create(playbook Playbook, userID string) (string, error) {
|
|
||||||
playbook.CreateAt = model.GetMillis()
|
|
||||||
playbook.UpdateAt = playbook.CreateAt
|
|
||||||
|
|
||||||
newID, err := s.store.Create(playbook)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
playbook.ID = newID
|
|
||||||
|
|
||||||
s.telemetry.CreatePlaybook(playbook, userID)
|
|
||||||
|
|
||||||
s.poster.PublishWebsocketEventToTeam(playbookCreatedWSEvent, map[string]interface{}{
|
|
||||||
"teamID": playbook.TeamID,
|
|
||||||
}, playbook.TeamID)
|
|
||||||
|
|
||||||
s.metricsService.IncrementPlaybookCreatedCount(1)
|
|
||||||
return newID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *playbookService) Import(playbook Playbook, userID string) (string, error) {
|
|
||||||
newID, err := s.Create(playbook, userID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
playbook.ID = newID
|
|
||||||
s.telemetry.ImportPlaybook(playbook, userID)
|
|
||||||
return newID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *playbookService) Get(id string) (Playbook, error) {
|
|
||||||
return s.store.Get(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *playbookService) GetPlaybooks() ([]Playbook, error) {
|
|
||||||
return s.store.GetPlaybooks()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *playbookService) GetPlaybooksForTeam(requesterInfo RequesterInfo, teamID string, opts PlaybookFilterOptions) (GetPlaybooksResults, error) {
|
|
||||||
return s.store.GetPlaybooksForTeam(requesterInfo, teamID, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *playbookService) Update(playbook Playbook, userID string) error {
|
|
||||||
if playbook.DeleteAt != 0 {
|
|
||||||
return errors.New("cannot update a playbook that is archived")
|
|
||||||
}
|
|
||||||
|
|
||||||
playbook.UpdateAt = model.GetMillis()
|
|
||||||
|
|
||||||
if err := s.store.Update(playbook); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.telemetry.UpdatePlaybook(playbook, userID)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *playbookService) Archive(playbook Playbook, userID string) error {
|
|
||||||
if playbook.ID == "" {
|
|
||||||
return errors.New("can't archive a playbook without an ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.store.Archive(playbook.ID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.telemetry.DeletePlaybook(playbook, userID)
|
|
||||||
s.metricsService.IncrementPlaybookArchivedCount(1)
|
|
||||||
|
|
||||||
s.poster.PublishWebsocketEventToTeam(playbookArchivedWSEvent, map[string]interface{}{
|
|
||||||
"teamID": playbook.TeamID,
|
|
||||||
}, playbook.TeamID)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *playbookService) Restore(playbook Playbook, userID string) error {
|
|
||||||
if playbook.ID == "" {
|
|
||||||
return errors.New("can't restore a playbook without an ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
if playbook.DeleteAt == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.store.Restore(playbook.ID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.telemetry.RestorePlaybook(playbook, userID)
|
|
||||||
s.metricsService.IncrementPlaybookRestoredCount(1)
|
|
||||||
|
|
||||||
s.poster.PublishWebsocketEventToTeam(playbookRestoredWSEvent, map[string]interface{}{
|
|
||||||
"teamID": playbook.TeamID,
|
|
||||||
}, playbook.TeamID)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AutoFollow method lets user to auto-follow all runs of a specific playbook
|
|
||||||
func (s *playbookService) AutoFollow(playbookID, userID string) error {
|
|
||||||
if err := s.store.AutoFollow(playbookID, userID); err != nil {
|
|
||||||
return errors.Wrapf(err, "user `%s` failed to auto-follow the playbook `%s`", userID, playbookID)
|
|
||||||
}
|
|
||||||
|
|
||||||
playbook, err := s.store.Get(playbookID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to retrieve playbook run")
|
|
||||||
}
|
|
||||||
s.telemetry.AutoFollowPlaybook(playbook, userID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AutoUnfollow method lets user to not auto-follow the newly created playbook runs
|
|
||||||
func (s *playbookService) AutoUnfollow(playbookID, userID string) error {
|
|
||||||
if err := s.store.AutoUnfollow(playbookID, userID); err != nil {
|
|
||||||
return errors.Wrapf(err, "user `%s` failed to auto-unfollow the playbook `%s`", userID, playbookID)
|
|
||||||
}
|
|
||||||
|
|
||||||
playbook, err := s.store.Get(playbookID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to retrieve playbook run")
|
|
||||||
}
|
|
||||||
s.telemetry.AutoUnfollowPlaybook(playbook, userID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAutoFollows returns list of users who auto-follow a playbook
|
|
||||||
func (s *playbookService) GetAutoFollows(playbookID string) ([]string, error) {
|
|
||||||
autoFollows, err := s.store.GetAutoFollows(playbookID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed to get auto-follows for the playbook `%s`", playbookID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return autoFollows, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duplicate duplicates a playbook
|
|
||||||
func (s *playbookService) Duplicate(playbook Playbook, userID string) (string, error) {
|
|
||||||
logger := logrus.WithFields(logrus.Fields{
|
|
||||||
"original_playbook_id": playbook.ID,
|
|
||||||
"user_id": userID,
|
|
||||||
})
|
|
||||||
|
|
||||||
newPlaybook := playbook.Clone()
|
|
||||||
newPlaybook.ID = ""
|
|
||||||
// Empty metric IDs if there are such. Otherwise, metrics will not be saved in the database.
|
|
||||||
for i := range newPlaybook.Metrics {
|
|
||||||
newPlaybook.Metrics[i].ID = ""
|
|
||||||
}
|
|
||||||
newPlaybook.Title = "Copy of " + playbook.Title
|
|
||||||
|
|
||||||
// On duplicating, make the current user the administrator.
|
|
||||||
newPlaybook.Members = []PlaybookMember{{
|
|
||||||
UserID: userID,
|
|
||||||
Roles: []string{PlaybookRoleMember, PlaybookRoleAdmin},
|
|
||||||
}}
|
|
||||||
|
|
||||||
playbookID, err := s.Create(newPlaybook, userID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.WithField("playbook_id", playbookID).Debug("Duplicated playbook")
|
|
||||||
return playbookID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// get top playbooks for teams
|
|
||||||
func (s *playbookService) GetTopPlaybooksForTeam(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error) {
|
|
||||||
permissionFlag, err := licenseAndGuestCheck(s, userID, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !permissionFlag {
|
|
||||||
return nil, errors.New("User cannot access playbooks insights")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.store.GetTopPlaybooksForTeam(teamID, userID, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get top playbooks for users
|
|
||||||
func (s *playbookService) GetTopPlaybooksForUser(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error) {
|
|
||||||
permissionFlag, err := licenseAndGuestCheck(s, userID, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !permissionFlag {
|
|
||||||
return nil, errors.New("User cannot access playbooks insights")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.store.GetTopPlaybooksForUser(teamID, userID, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func licenseAndGuestCheck(s *playbookService, userID string, isMyInsights bool) (bool, error) {
|
|
||||||
licenseError := errors.New("invalid license/authorization to use insights API")
|
|
||||||
guestError := errors.New("Guests aren't authorized to use insights API")
|
|
||||||
lic := s.api.GetLicense()
|
|
||||||
|
|
||||||
user, err := s.api.GetUserByID(userID)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.IsGuest() {
|
|
||||||
return false, guestError
|
|
||||||
}
|
|
||||||
|
|
||||||
if lic == nil && !isMyInsights {
|
|
||||||
return false, licenseError
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isMyInsights && (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) {
|
|
||||||
return false, licenseError
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPlaybook_MarshalJSON(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
original Playbook
|
|
||||||
expected []byte
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "marshals a struct with nil slices into empty arrays",
|
|
||||||
original: Playbook{
|
|
||||||
ID: "playbookid",
|
|
||||||
Title: "the playbook title",
|
|
||||||
Description: "the playbook's description",
|
|
||||||
TeamID: "theteamid",
|
|
||||||
CreatePublicPlaybookRun: true,
|
|
||||||
CreateAt: 4503134,
|
|
||||||
DeleteAt: 0,
|
|
||||||
NumStages: 0,
|
|
||||||
NumSteps: 0,
|
|
||||||
Checklists: nil,
|
|
||||||
Members: nil,
|
|
||||||
BroadcastChannelIDs: []string{"channelid"},
|
|
||||||
ReminderMessageTemplate: "This is a message",
|
|
||||||
ReminderTimerDefaultSeconds: 0,
|
|
||||||
InvitedUserIDs: nil,
|
|
||||||
InvitedGroupIDs: nil,
|
|
||||||
},
|
|
||||||
expected: []byte(`"checklists":[]`),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "marshals a struct with nil []checklistItems into an empty array",
|
|
||||||
original: Playbook{
|
|
||||||
ID: "playbookid",
|
|
||||||
Title: "the playbook title",
|
|
||||||
Description: "the playbook's description",
|
|
||||||
TeamID: "theteamid",
|
|
||||||
CreatePublicPlaybookRun: true,
|
|
||||||
CreateAt: 4503134,
|
|
||||||
DeleteAt: 0,
|
|
||||||
NumStages: 0,
|
|
||||||
NumSteps: 0,
|
|
||||||
Checklists: []Checklist{
|
|
||||||
{
|
|
||||||
ID: "checklist1",
|
|
||||||
Title: "checklist 1",
|
|
||||||
Items: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
BroadcastChannelIDs: []string{},
|
|
||||||
ReminderMessageTemplate: "This is a message",
|
|
||||||
ReminderTimerDefaultSeconds: 0,
|
|
||||||
InvitedUserIDs: nil,
|
|
||||||
InvitedGroupIDs: nil,
|
|
||||||
WebhookOnStatusUpdateURLs: []string{"testurl"},
|
|
||||||
WebhookOnStatusUpdateEnabled: true,
|
|
||||||
},
|
|
||||||
expected: []byte(`"checklists":[{"id":"checklist1","title":"checklist 1","items":[]}]`),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := json.Marshal(tt.original)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.Contains(t, string(got), string(tt.expected))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlaybookFilterOptions_Clone(t *testing.T) {
|
|
||||||
options := PlaybookFilterOptions{
|
|
||||||
Page: 1,
|
|
||||||
PerPage: 10,
|
|
||||||
Sort: SortByID,
|
|
||||||
Direction: DirectionAsc,
|
|
||||||
}
|
|
||||||
marshalledOptions, err := json.Marshal(options)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
clone := options.Clone()
|
|
||||||
clone.Page = 2
|
|
||||||
clone.PerPage = 20
|
|
||||||
clone.Sort = SortByName
|
|
||||||
clone.Direction = DirectionDesc
|
|
||||||
|
|
||||||
var unmarshalledOptions PlaybookFilterOptions
|
|
||||||
err = json.Unmarshal(marshalledOptions, &unmarshalledOptions)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, options, unmarshalledOptions)
|
|
||||||
require.NotEqual(t, clone, unmarshalledOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlaybookFilterOptions_Validate(t *testing.T) {
|
|
||||||
t.Run("non-positive PerPage", func(t *testing.T) {
|
|
||||||
options := PlaybookFilterOptions{
|
|
||||||
PerPage: -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
validOptions, err := options.Validate()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, PerPageDefault, validOptions.PerPage)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid sort option", func(t *testing.T) {
|
|
||||||
options := PlaybookFilterOptions{
|
|
||||||
Sort: SortField("invalid"),
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := options.Validate()
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid, but wrong case sort option", func(t *testing.T) {
|
|
||||||
options := PlaybookFilterOptions{
|
|
||||||
Sort: SortField("STAges"),
|
|
||||||
}
|
|
||||||
|
|
||||||
validOptions, err := options.Validate()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, SortByStages, validOptions.Sort)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid, no explicit sort option", func(t *testing.T) {
|
|
||||||
options := PlaybookFilterOptions{}
|
|
||||||
|
|
||||||
validOptions, err := options.Validate()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, SortByID, validOptions.Sort)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid sort direction", func(t *testing.T) {
|
|
||||||
options := PlaybookFilterOptions{
|
|
||||||
Direction: SortDirection("invalid"),
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := options.Validate()
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid, but wrong case direction option", func(t *testing.T) {
|
|
||||||
options := PlaybookFilterOptions{
|
|
||||||
Direction: SortDirection("DEsC"),
|
|
||||||
}
|
|
||||||
|
|
||||||
validOptions, err := options.Validate()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, DirectionDesc, validOptions.Direction)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid, no explicit direction", func(t *testing.T) {
|
|
||||||
options := PlaybookFilterOptions{}
|
|
||||||
|
|
||||||
validOptions, err := options.Validate()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, DirectionAsc, validOptions.Direction)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid", func(t *testing.T) {
|
|
||||||
options := PlaybookFilterOptions{
|
|
||||||
Page: 1,
|
|
||||||
PerPage: 10,
|
|
||||||
Sort: SortByTitle,
|
|
||||||
Direction: DirectionAsc,
|
|
||||||
}
|
|
||||||
|
|
||||||
validOptions, err := options.Validate()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, options, validOptions)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrChannelNotFound = errors.Errorf("channel not found")
|
|
||||||
ErrChannelDeleted = errors.Errorf("channel deleted")
|
|
||||||
ErrChannelNotInExpectedTeam = errors.Errorf("channel in different team")
|
|
||||||
)
|
|
||||||
|
|
||||||
func IsChannelActiveInTeam(channelID string, expectedTeamID string, api playbooks.ServicesAPI) error {
|
|
||||||
channel, err := api.GetChannelByID(channelID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(ErrChannelNotFound, "channel with ID %s does not exist", channelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if channel.DeleteAt != 0 {
|
|
||||||
return errors.Wrapf(ErrChannelDeleted, "channel with ID %s is archived", channelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if channel.TeamId != expectedTeamID {
|
|
||||||
return errors.Wrapf(ErrChannelNotInExpectedTeam,
|
|
||||||
"channel with ID %s is on team with ID %s; expected team ID is %s",
|
|
||||||
channelID,
|
|
||||||
channel.TeamId,
|
|
||||||
expectedTeamID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ShouldSendWeeklyDigestMessage(userInfo UserInfo, timezone *time.Location, currentTime time.Time) bool {
|
|
||||||
if userInfo.DigestNotificationSettings.DisableWeeklyDigest {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
lastSentTime := time.UnixMilli(userInfo.LastDailyTodoDMAt).In(timezone)
|
|
||||||
|
|
||||||
currentYear, currentWeek := currentTime.ISOWeek()
|
|
||||||
lastSentYear, lastSentWeek := lastSentTime.ISOWeek()
|
|
||||||
isFirstLoginOfTheWeek := currentYear != lastSentYear || currentWeek != lastSentWeek
|
|
||||||
|
|
||||||
return isFirstLoginOfTheWeek
|
|
||||||
}
|
|
||||||
|
|
||||||
func ShouldSendDailyDigestMessage(userInfo UserInfo, timezone *time.Location, currentTime time.Time) bool {
|
|
||||||
if userInfo.DigestNotificationSettings.DisableDailyDigest {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// DM message if it's the next day and been more than an hour since the last post
|
|
||||||
// Hat tip to Github plugin for the logic.
|
|
||||||
lastSentTime := time.UnixMilli(userInfo.LastDailyTodoDMAt).In(timezone)
|
|
||||||
|
|
||||||
isMoreThanOneHourPassed := currentTime.Sub(lastSentTime).Hours() >= 1
|
|
||||||
|
|
||||||
isDifferentDay := currentTime.Day() != lastSentTime.Day() ||
|
|
||||||
currentTime.Month() != lastSentTime.Month() ||
|
|
||||||
currentTime.Year() != lastSentTime.Year()
|
|
||||||
|
|
||||||
return isMoreThanOneHourPassed && isDifferentDay
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestShouldSendWeeklyDigestMessage(t *testing.T) {
|
|
||||||
now, ok := time.Parse("2006-01-02", "2022-10-08")
|
|
||||||
if ok != nil {
|
|
||||||
t.Error("Could not parse current time")
|
|
||||||
}
|
|
||||||
|
|
||||||
type args struct {
|
|
||||||
userInfo UserInfo
|
|
||||||
timezone *time.Location
|
|
||||||
currentTime time.Time
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Should not send a weekly digest if the user has configured it so",
|
|
||||||
args: args{
|
|
||||||
userInfo: UserInfo{
|
|
||||||
ID: "testUser",
|
|
||||||
LastDailyTodoDMAt: now.AddDate(0, 0, -6).UnixMilli(),
|
|
||||||
DigestNotificationSettings: DigestNotificationSettings{
|
|
||||||
DisableWeeklyDigest: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
timezone: time.FixedZone("local", 0),
|
|
||||||
currentTime: now,
|
|
||||||
},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should not send a weekly digest if we have already sent a digest this week",
|
|
||||||
args: args{
|
|
||||||
userInfo: UserInfo{
|
|
||||||
ID: "testUser",
|
|
||||||
LastDailyTodoDMAt: now.AddDate(0, 0, -1).UnixMilli(),
|
|
||||||
DigestNotificationSettings: DigestNotificationSettings{
|
|
||||||
DisableDailyDigest: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
timezone: time.FixedZone("local", 0),
|
|
||||||
currentTime: now,
|
|
||||||
},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should send a weekly digest if we have not sent a digest this week",
|
|
||||||
args: args{
|
|
||||||
userInfo: UserInfo{
|
|
||||||
ID: "testUser",
|
|
||||||
LastDailyTodoDMAt: now.AddDate(0, 0, -6).UnixMilli(),
|
|
||||||
DigestNotificationSettings: DigestNotificationSettings{
|
|
||||||
DisableDailyDigest: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
timezone: time.FixedZone("local", 0),
|
|
||||||
currentTime: now,
|
|
||||||
},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should send a weekly digest if we have not sent a digest ever",
|
|
||||||
args: args{
|
|
||||||
userInfo: UserInfo{
|
|
||||||
ID: "testUser",
|
|
||||||
LastDailyTodoDMAt: 0,
|
|
||||||
DigestNotificationSettings: DigestNotificationSettings{
|
|
||||||
DisableDailyDigest: false,
|
|
||||||
DisableWeeklyDigest: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
timezone: time.FixedZone("local", 0),
|
|
||||||
currentTime: now,
|
|
||||||
},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := ShouldSendWeeklyDigestMessage(tt.args.userInfo, tt.args.timezone, tt.args.currentTime); got != tt.want {
|
|
||||||
t.Errorf("ShouldSendWeeklyDigestMessage() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShouldSendDailyDigestMessage(t *testing.T) {
|
|
||||||
now, ok := time.Parse("Jan 2, 2006 at 3:04pm", "Oct 8, 2022 at 3:04pm")
|
|
||||||
lateNow, lateOk := time.Parse("Jan 2, 2006 at 3:04pm", "Oct 8, 2022 at 12:10am")
|
|
||||||
if ok != nil || lateOk != nil {
|
|
||||||
t.Error("Could not parse current time")
|
|
||||||
}
|
|
||||||
|
|
||||||
type args struct {
|
|
||||||
userInfo UserInfo
|
|
||||||
timezone *time.Location
|
|
||||||
currentTime time.Time
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Should not send a daily digest if we have already sent a digest today",
|
|
||||||
args: args{
|
|
||||||
userInfo: UserInfo{
|
|
||||||
ID: "testUser",
|
|
||||||
LastDailyTodoDMAt: now.Add(-((time.Hour * 1) + (time.Minute * 2))).UnixMilli(),
|
|
||||||
DigestNotificationSettings: DigestNotificationSettings{
|
|
||||||
DisableDailyDigest: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
timezone: time.FixedZone("local", 0),
|
|
||||||
currentTime: now,
|
|
||||||
},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should send a daily digest if we have not sent a digest today",
|
|
||||||
args: args{
|
|
||||||
userInfo: UserInfo{
|
|
||||||
ID: "testUser",
|
|
||||||
LastDailyTodoDMAt: now.Add(-(time.Hour * 25)).UnixMilli(),
|
|
||||||
DigestNotificationSettings: DigestNotificationSettings{
|
|
||||||
DisableDailyDigest: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
timezone: time.FixedZone("local", 0),
|
|
||||||
currentTime: now,
|
|
||||||
},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should not send a daily digest if we have sent one within the last hour",
|
|
||||||
args: args{
|
|
||||||
userInfo: UserInfo{
|
|
||||||
ID: "testUser",
|
|
||||||
LastDailyTodoDMAt: lateNow.Add(-(time.Minute * 40)).UnixMilli(),
|
|
||||||
DigestNotificationSettings: DigestNotificationSettings{
|
|
||||||
DisableDailyDigest: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
timezone: time.FixedZone("local", 0),
|
|
||||||
currentTime: lateNow,
|
|
||||||
},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := ShouldSendDailyDigestMessage(tt.args.userInfo, tt.args.timezone, tt.args.currentTime); got != tt.want {
|
|
||||||
t.Errorf("ShouldSendDailyDigestMessage() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const RetrospectivePrefix = "retro_"
|
|
||||||
|
|
||||||
// HandleReminder is the handler for all reminder events.
|
|
||||||
func (s *PlaybookRunServiceImpl) HandleReminder(key string) {
|
|
||||||
if strings.HasPrefix(key, RetrospectivePrefix) {
|
|
||||||
s.handleReminderToFillRetro(strings.TrimPrefix(key, RetrospectivePrefix))
|
|
||||||
} else {
|
|
||||||
s.handleStatusUpdateReminder(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunServiceImpl) handleReminderToFillRetro(playbookRunID string) {
|
|
||||||
logger := logrus.WithField("playbook_run_id", playbookRunID)
|
|
||||||
|
|
||||||
playbookRunToRemind, err := s.GetPlaybookRun(playbookRunID)
|
|
||||||
if err != nil {
|
|
||||||
logger.WithError(err).Errorf("handleReminderToFillRetro failed to get playbook run")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// In the meantime we did publish a retrospective, so no reminder.
|
|
||||||
if playbookRunToRemind.RetrospectivePublishedAt != 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are not in the finished state then don't remind
|
|
||||||
if playbookRunToRemind.CurrentStatus != StatusFinished {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = s.postRetrospectiveReminder(playbookRunToRemind, false); err != nil {
|
|
||||||
logger.WithError(err).Errorf("couldn't post reminder")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jobs can't be rescheduled within themselves with the same key. As a temporary workaround do it in a delayed goroutine
|
|
||||||
go func() {
|
|
||||||
time.Sleep(time.Second * 2)
|
|
||||||
if err = s.SetReminder(RetrospectivePrefix+playbookRunID, time.Duration(playbookRunToRemind.RetrospectiveReminderIntervalSeconds)*time.Second); err != nil {
|
|
||||||
logger.WithError(err).Errorf("failed to reocurr retrospective reminder")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunServiceImpl) handleStatusUpdateReminder(playbookRunID string) {
|
|
||||||
logger := logrus.WithField("playbook_run_id", playbookRunID)
|
|
||||||
|
|
||||||
playbookRunToModify, err := s.GetPlaybookRun(playbookRunID)
|
|
||||||
if err != nil {
|
|
||||||
logger.WithError(err).Error("HandleReminder failed to get playbook run")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
owner, err := s.api.GetUserByID(playbookRunToModify.OwnerUserID)
|
|
||||||
if err != nil {
|
|
||||||
logger.WithError(err).WithField("user_id", playbookRunToModify.OwnerUserID).Error("HandleReminder failed to get owner")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
attachments := []*model.SlackAttachment{
|
|
||||||
{
|
|
||||||
Actions: []*model.PostAction{
|
|
||||||
{
|
|
||||||
Type: "button",
|
|
||||||
Name: "Update status",
|
|
||||||
Integration: &model.PostActionIntegration{
|
|
||||||
URL: fmt.Sprintf("/plugins/%s/api/v0/runs/%s/reminder/button-update",
|
|
||||||
"playbooks",
|
|
||||||
playbookRunToModify.ID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
post := &model.Post{
|
|
||||||
Message: fmt.Sprintf("@%s, please provide a status update for [%s](%s).", owner.Username, playbookRunToModify.Name, GetRunDetailsRelativeURL(playbookRunID)),
|
|
||||||
ChannelId: playbookRunToModify.ChannelID,
|
|
||||||
Type: "custom_update_status",
|
|
||||||
Props: map[string]any{
|
|
||||||
"targetUsername": owner.Username,
|
|
||||||
"playbookRunId": playbookRunToModify.ID,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
model.ParseSlackAttachment(post, attachments)
|
|
||||||
|
|
||||||
if err = s.poster.PostMessageToThread("", post); err != nil {
|
|
||||||
logger.WithError(err).Errorf("HandleReminder error posting reminder message")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// broadcast to followers
|
|
||||||
message, err := s.buildOverdueStatusUpdateMessage(playbookRunToModify, owner.Username)
|
|
||||||
if err != nil {
|
|
||||||
logger.WithError(err).Error("failed to build overdue status update message")
|
|
||||||
} else {
|
|
||||||
err = s.dmPostToRunFollowers(&model.Post{Message: message}, overdueStatusUpdateMessage, playbookRunToModify.ID, "")
|
|
||||||
if err != nil {
|
|
||||||
logger.WithError(err).Error("failed to dm post to run followers")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookRunToModify.ReminderPostID = post.Id
|
|
||||||
if _, err = s.store.UpdatePlaybookRun(playbookRunToModify); err != nil {
|
|
||||||
logger.WithError(err).Error("error updating with reminder post id")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunServiceImpl) buildOverdueStatusUpdateMessage(playbookRun *PlaybookRun, ownerUserName string) (string, error) {
|
|
||||||
channel, err := s.api.GetChannelByID(playbookRun.ChannelID)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrapf(err, "can't get channel - %s", playbookRun.ChannelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
team, err := s.api.GetTeam(channel.TeamId)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrapf(err, "can't get team - %s", channel.TeamId)
|
|
||||||
}
|
|
||||||
|
|
||||||
message := fmt.Sprintf("Status update is overdue for [%s](/%s/channels/%s?telem_action=todo_overduestatus_clicked&telem_run_id=%s&forceRHSOpen) (Owner: @%s)\n",
|
|
||||||
channel.DisplayName, team.Name, channel.Name, playbookRun.ID, ownerUserName)
|
|
||||||
|
|
||||||
return message, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReminder sets a reminder. After timeInMinutes in the future, the owner will be
|
|
||||||
// reminded to update the playbook run's status.
|
|
||||||
func (s *PlaybookRunServiceImpl) SetReminder(playbookRunID string, fromNow time.Duration) error {
|
|
||||||
if _, err := s.scheduler.ScheduleOnce(playbookRunID, time.Now().Add(fromNow)); err != nil {
|
|
||||||
return errors.Wrap(err, "unable to schedule reminder")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveReminder removes the pending reminder for the given playbook run, if any.
|
|
||||||
func (s *PlaybookRunServiceImpl) RemoveReminder(playbookRunID string) {
|
|
||||||
s.scheduler.Cancel(playbookRunID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// resetReminderTimer sets the previous reminder timer to 0.
|
|
||||||
func (s *PlaybookRunServiceImpl) resetReminderTimer(playbookRunID string) error {
|
|
||||||
playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to retrieve playbook run")
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookRunToModify.PreviousReminder = 0
|
|
||||||
|
|
||||||
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to update playbook run after resetting reminder timer")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.poster.PublishWebsocketEventToChannel(playbookRunUpdatedWSEvent, playbookRunToModify, playbookRunToModify.ChannelID)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetReminder creates a timeline event for a reminder being reset and then creates a new reminder
|
|
||||||
func (s *PlaybookRunServiceImpl) ResetReminder(playbookRunID string, newReminder time.Duration) error {
|
|
||||||
playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to retrieve playbook run")
|
|
||||||
}
|
|
||||||
|
|
||||||
eventTime := model.GetMillis()
|
|
||||||
event := &TimelineEvent{
|
|
||||||
PlaybookRunID: playbookRunToModify.ID,
|
|
||||||
CreateAt: eventTime,
|
|
||||||
EventAt: eventTime,
|
|
||||||
EventType: StatusUpdateSnoozed,
|
|
||||||
SubjectUserID: playbookRunToModify.ReporterUserID,
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := s.store.CreateTimelineEvent(event); err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to create timeline event after resetting reminder timer")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.SetNewReminder(playbookRunID, newReminder)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNewReminder sets a new reminder for playbookRunID, removes any pending reminder, removes the
|
|
||||||
// reminder post in the playbookRun's channel, and resets the PreviousReminder and
|
|
||||||
// LastStatusUpdateAt (so the countdown timer to "update due" shows the correct time)
|
|
||||||
func (s *PlaybookRunServiceImpl) SetNewReminder(playbookRunID string, newReminder time.Duration) error {
|
|
||||||
playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to retrieve playbook run")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove pending reminder (if any)
|
|
||||||
s.RemoveReminder(playbookRunID)
|
|
||||||
|
|
||||||
// Remove reminder post (if any)
|
|
||||||
if playbookRunToModify.ReminderPostID != "" {
|
|
||||||
if err = s.removePost(playbookRunToModify.ReminderPostID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
playbookRunToModify.ReminderPostID = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
playbookRunToModify.PreviousReminder = newReminder
|
|
||||||
playbookRunToModify.LastStatusUpdateAt = model.GetMillis()
|
|
||||||
|
|
||||||
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to update playbook run after resetting reminder timer")
|
|
||||||
}
|
|
||||||
|
|
||||||
if newReminder != 0 {
|
|
||||||
if err = s.SetReminder(playbookRunID, newReminder); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to set the reminder for playbook run")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.poster.PublishWebsocketEventToChannel(playbookRunUpdatedWSEvent, playbookRunToModify, playbookRunToModify.ChannelID)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlaybookRunServiceImpl) removePost(postID string) error {
|
|
||||||
post, err := s.api.GetPost(postID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to retrieve reminder post %s", postID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if post.DeleteAt != 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = s.api.DeletePost(postID); err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to delete reminder post %s", postID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
// SortField enumerates the available fields we can sort on.
|
|
||||||
type SortField string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// SortByTitle sorts by the title field of a playbook.
|
|
||||||
SortByTitle SortField = "title"
|
|
||||||
|
|
||||||
// SortByStages sorts by the number of checklists in a playbook.
|
|
||||||
SortByStages SortField = "stages"
|
|
||||||
|
|
||||||
// SortBySteps sorts by the number of steps in a playbook.
|
|
||||||
SortBySteps SortField = "steps"
|
|
||||||
|
|
||||||
// SortByRuns sorts by the number of times a playbook has been run.
|
|
||||||
SortByRuns SortField = "runs"
|
|
||||||
|
|
||||||
// SortByCreateAt sorts by the created time of a playbook or playbook run.
|
|
||||||
SortByCreateAt SortField = "create_at"
|
|
||||||
|
|
||||||
// SortByID sorts by the primary key of a playbook or playbook run.
|
|
||||||
SortByID SortField = "id"
|
|
||||||
|
|
||||||
// SortByName sorts by the name of a playbook run.
|
|
||||||
SortByName SortField = "name"
|
|
||||||
|
|
||||||
// SortByOwnerUserID sorts by the user id of the owner of a playbook run.
|
|
||||||
SortByOwnerUserID SortField = "owner_user_id"
|
|
||||||
|
|
||||||
// SortByTeamID sorts by the team id of a playbook or playbook run.
|
|
||||||
SortByTeamID SortField = "team_id"
|
|
||||||
|
|
||||||
// SortByEndAt sorts by the end time of a playbook run.
|
|
||||||
SortByEndAt SortField = "end_at"
|
|
||||||
|
|
||||||
// SortByStatus sorts by the status of a playbook run.
|
|
||||||
SortByStatus SortField = "status"
|
|
||||||
|
|
||||||
// SortByLastStatusUpdateAt sorts by when the playbook run was last updated.
|
|
||||||
SortByLastStatusUpdateAt SortField = "last_status_update_at"
|
|
||||||
|
|
||||||
// SortByLastStatusUpdateAt sorts by when the playbook was last run.
|
|
||||||
SortByLastRunAt SortField = "last_run_at"
|
|
||||||
|
|
||||||
// SortByActiveRuns sorts by number of active runs in the playbook.
|
|
||||||
SortByActiveRuns SortField = "active_runs"
|
|
||||||
|
|
||||||
// SortByMetric0 ..3 sorts by the playbook's metric index
|
|
||||||
SortByMetric0 SortField = "metric0"
|
|
||||||
SortByMetric1 SortField = "metric1"
|
|
||||||
SortByMetric2 SortField = "metric2"
|
|
||||||
SortByMetric3 SortField = "metric3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SortDirection is the type used to specify the ascending or descending order of returned results.
|
|
||||||
type SortDirection string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DirectionDesc is descending order.
|
|
||||||
DirectionDesc SortDirection = "DESC"
|
|
||||||
|
|
||||||
// DirectionAsc is ascending order.
|
|
||||||
DirectionAsc SortDirection = "ASC"
|
|
||||||
)
|
|
||||||
|
|
||||||
func IsValidDirection(direction SortDirection) bool {
|
|
||||||
return direction == DirectionAsc || direction == DirectionDesc
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TaskAction struct {
|
|
||||||
Trigger Trigger `json:"trigger"`
|
|
||||||
Actions []Action `json:"actions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaskActionType string
|
|
||||||
type TaskTriggerType string
|
|
||||||
|
|
||||||
type Trigger struct {
|
|
||||||
Type TaskTriggerType `json:"type"`
|
|
||||||
// Payload is the json payload that stores trigger specific settings or config.
|
|
||||||
// This should be unmarshalled into a concrete type during usage
|
|
||||||
Payload string `json:"payload"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Action struct {
|
|
||||||
Type TaskActionType `json:"type"`
|
|
||||||
// Payload is the json payload that stores action specific settings or config.
|
|
||||||
// This should be unmarshalled into a concrete type during usage
|
|
||||||
Payload string `json:"payload"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Known Types
|
|
||||||
const (
|
|
||||||
KeywordsByUsersTriggerType TaskTriggerType = "keywords_by_users"
|
|
||||||
|
|
||||||
MarkItemAsDoneActionType TaskActionType = "mark_item_as_done"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ValidTaskActionTypes = []TaskActionType{
|
|
||||||
MarkItemAsDoneActionType,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Triggers
|
|
||||||
type KeywordsByUsersTrigger struct {
|
|
||||||
typ TaskTriggerType
|
|
||||||
Payload KeywordsByUsersTriggerPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeywordsByUsersTriggerPayload struct {
|
|
||||||
Keywords []string `json:"keywords" mapstructure:"keywords"`
|
|
||||||
UserIDs []string `json:"user_ids" mapstructure:"user_ids"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewKeywordsByUsersTrigger(trigger Trigger) (*KeywordsByUsersTrigger, error) {
|
|
||||||
if trigger.Type != KeywordsByUsersTriggerType {
|
|
||||||
return nil, errors.Errorf("Unexpected trigger type: %s, expected: %s", trigger.Type, KeywordsByUsersTriggerType)
|
|
||||||
}
|
|
||||||
var t KeywordsByUsersTrigger
|
|
||||||
t.typ = KeywordsByUsersTriggerType
|
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(trigger.Payload), &t.Payload); err != nil {
|
|
||||||
return nil, errors.New("unable to decode payload from trigger")
|
|
||||||
}
|
|
||||||
return &t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *KeywordsByUsersTrigger) IsValid() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *KeywordsByUsersTrigger) IsTriggered(post *model.Post) bool {
|
|
||||||
foundUser := false
|
|
||||||
if len(t.Payload.UserIDs) > 0 {
|
|
||||||
for _, userID := range t.Payload.UserIDs {
|
|
||||||
if post.UserId == userID {
|
|
||||||
foundUser = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
foundUser = true
|
|
||||||
}
|
|
||||||
if foundUser {
|
|
||||||
for _, keyword := range t.Payload.Keywords {
|
|
||||||
if strings.Contains(post.Message, keyword) {
|
|
||||||
logrus.WithField("keyword", keyword)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
type MarkItemAsDoneAction struct {
|
|
||||||
typ TaskActionType
|
|
||||||
Payload MarkItemAsDoneActionPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarkItemAsDoneActionPayload struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMarkItemAsDoneAction(action Action) (*MarkItemAsDoneAction, error) {
|
|
||||||
if action.Type != MarkItemAsDoneActionType {
|
|
||||||
return nil, errors.Errorf("Unexpected trigger type: %s, expected: %s", action.Type, MarkItemAsDoneActionType)
|
|
||||||
}
|
|
||||||
var a MarkItemAsDoneAction
|
|
||||||
a.typ = MarkItemAsDoneActionType
|
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(action.Payload), &a.Payload); err != nil {
|
|
||||||
return nil, errors.New("unable to decode payload from trigger")
|
|
||||||
}
|
|
||||||
return &a, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *MarkItemAsDoneAction) IsValid() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validators
|
|
||||||
func ValidateTrigger(t Trigger) error {
|
|
||||||
switch t.Type {
|
|
||||||
case KeywordsByUsersTriggerType:
|
|
||||||
trigger, err := NewKeywordsByUsersTrigger(t)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return trigger.IsValid()
|
|
||||||
default:
|
|
||||||
return errors.Errorf("Unknown task trigger type: %s", t.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateAction(a Action) error {
|
|
||||||
switch a.Type {
|
|
||||||
case MarkItemAsDoneActionType:
|
|
||||||
action, err := NewMarkItemAsDoneAction(a)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return action.IsValid()
|
|
||||||
default:
|
|
||||||
return errors.Errorf("Unknown task action type: %s", a.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user