mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
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
This commit is contained in:
parent
f386ef1ea8
commit
44a99d1736
879 changed files with 66 additions and 127144 deletions
|
|
@ -148,6 +148,7 @@ const prepackagedPlugins = [
|
|||
'com.mattermost.nps',
|
||||
'com.mattermost.welcomebot',
|
||||
'zoom',
|
||||
'playbooks',
|
||||
];
|
||||
|
||||
Cypress.Commands.add('apiDisableNonPrepackagedPlugins', () => {
|
||||
|
|
|
|||
|
|
@ -606,9 +606,7 @@ const defaultServerConfig: AdminConfig = {
|
|||
CleanupJobsThresholdDays: -1,
|
||||
CleanupConfigThresholdDays: -1,
|
||||
},
|
||||
ProductSettings: {
|
||||
EnablePlaybooks: true,
|
||||
},
|
||||
ProductSettings: {},
|
||||
PluginSettings: {
|
||||
Enable: true,
|
||||
EnableUploads: false,
|
||||
|
|
|
|||
|
|
@ -147,8 +147,7 @@ DIST_PATH_WIN=$(DIST_ROOT)/windows/mattermost
|
|||
TESTS=.
|
||||
|
||||
# Packages lists
|
||||
TE_PACKAGES=$(shell $(GO) list ./... | grep -vE 'server/v8/playbooks|server/v8/cmd/mmctl')
|
||||
PLAYBOOKS_PACKAGES=$(shell $(GO) list ./... | grep -E 'server/v8/playbooks')
|
||||
TE_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')
|
||||
|
||||
|
|
@ -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-github-v2.1.6
|
||||
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-jira-v3.2.5
|
||||
PLUGIN_PACKAGES += mattermost-plugin-jitsi-v2.0.1
|
||||
|
|
@ -188,9 +188,9 @@ endif
|
|||
EE_PACKAGES=$(shell $(GO) list $(BUILD_ENTERPRISE_DIR)/...)
|
||||
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
ALL_PACKAGES=$(TE_PACKAGES) $(PLAYBOOKS_PACKAGES) $(EE_PACKAGES)
|
||||
ALL_PACKAGES=$(TE_PACKAGES) $(EE_PACKAGES)
|
||||
else
|
||||
ALL_PACKAGES=$(TE_PACKAGES) $(PLAYBOOKS_PACKAGES)
|
||||
ALL_PACKAGES=$(TE_PACKAGES)
|
||||
endif
|
||||
|
||||
all: run ## Alias for 'run'.
|
||||
|
|
@ -460,7 +460,6 @@ endif
|
|||
|
||||
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)" "$(PLAYBOOKS_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "90m"
|
||||
ifneq ($(IS_CI),true)
|
||||
ifneq ($(MM_NO_DOCKER),true)
|
||||
ifneq ($(TEMP_DOCKER_SERVICES),)
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ func TestInstallPluginLocally(t *testing.T) {
|
|||
defer th.TearDown()
|
||||
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.Equal(t, "app.plugin.blocked.app_error", appErr.Id)
|
||||
assertBundleInfoManifests(t, th, []*model.Manifest{})
|
||||
|
|
|
|||
|
|
@ -83,16 +83,6 @@ func (s *Server) shouldStart(product string) bool {
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import (
|
|||
|
||||
// Blank imports for each product to register themselves
|
||||
_ "github.com/mattermost/mattermost/server/v8/boards/product"
|
||||
_ "github.com/mattermost/mattermost/server/v8/playbooks/product"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
|
|||
|
|
@ -144,8 +144,6 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
|
|||
props["AllowSyncedDrafts"] = strconv.FormatBool(*c.ServiceSettings.AllowSyncedDrafts)
|
||||
props["DelayChannelAutocomplete"] = strconv.FormatBool(*c.ExperimentalSettings.DelayChannelAutocomplete)
|
||||
|
||||
props["EnablePlaybooks"] = strconv.FormatBool(*c.ProductSettings.EnablePlaybooks)
|
||||
|
||||
if license != nil {
|
||||
props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer)
|
||||
|
||||
|
|
|
|||
|
|
@ -326,20 +326,6 @@ func TestGetClientConfig(t *testing.T) {
|
|||
"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 {
|
||||
|
|
|
|||
|
|
@ -810,6 +810,9 @@ func TestDiff(t *testing.T) {
|
|||
"com.mattermost.calls": {
|
||||
Enable: true,
|
||||
},
|
||||
"playbooks": {
|
||||
Enable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -839,6 +842,9 @@ func TestDiff(t *testing.T) {
|
|||
"com.mattermost.calls": {
|
||||
Enable: true,
|
||||
},
|
||||
"playbooks": {
|
||||
Enable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -860,6 +866,9 @@ func TestDiff(t *testing.T) {
|
|||
"com.mattermost.calls": {
|
||||
Enable: true,
|
||||
},
|
||||
"playbooks": {
|
||||
Enable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,13 +22,11 @@ require (
|
|||
github.com/golang-migrate/migrate/v4 v4.15.2
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
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/mux v1.8.0
|
||||
github.com/gorilla/schema v1.2.0
|
||||
github.com/gorilla/websocket v1.5.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/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c
|
||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
|
||||
|
|
@ -54,7 +52,6 @@ require (
|
|||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.23
|
||||
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/oov/psd v0.0.0-20220121172623-5db5eafcecbb
|
||||
github.com/opentracing/opentracing-go v1.2.0
|
||||
|
|
@ -78,17 +75,14 @@ require (
|
|||
github.com/uber/jaeger-lib v2.4.1+incompatible
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||
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/yuin/goldmark v1.5.4
|
||||
golang.org/x/crypto v0.8.0
|
||||
golang.org/x/image v0.7.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/term v0.8.0
|
||||
golang.org/x/tools v0.9.1
|
||||
gopkg.in/guregu/null.v4 v4.0.0
|
||||
gopkg.in/mail.v2 v2.3.1
|
||||
gopkg.in/olivere/elastic.v6 v6.2.37
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
|
|
@ -178,6 +172,7 @@ require (
|
|||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/sha256-simd v1.0.0 // 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/reflect2 v1.0.2 // 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/sys v0.8.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/grpc v1.54.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/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.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
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.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/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/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/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os=
|
||||
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/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/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/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=
|
||||
|
|
@ -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-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.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/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=
|
||||
|
|
@ -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.5/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/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=
|
||||
|
|
@ -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/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/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/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
|
|
|||
|
|
@ -4947,10 +4947,6 @@
|
|||
"id": "app.command.deletecommand.internal_error",
|
||||
"translation": "Unable to delete the command."
|
||||
},
|
||||
{
|
||||
"id": "app.command.execute.error",
|
||||
"translation": "Unable to execute command."
|
||||
},
|
||||
{
|
||||
"id": "app.command.getcommand.internal_error",
|
||||
"translation": "Unable to get the command."
|
||||
|
|
@ -6815,88 +6811,6 @@
|
|||
"id": "app.user.demote_user_to_guest.user_update.app_error",
|
||||
"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",
|
||||
"translation": "We encountered an error finding the account."
|
||||
|
|
@ -6973,26 +6887,6 @@
|
|||
"id": "app.user.missing_account.const",
|
||||
"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",
|
||||
"translation": "Unable to delete the existing account."
|
||||
|
|
@ -7005,108 +6899,6 @@
|
|||
"id": "app.user.promote_guest.user_update.app_error",
|
||||
"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",
|
||||
"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
vendored
3
server/playbooks/server/.gitignore
vendored
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
})
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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()
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue