From 117f681b10c75a59191529d566be73cacb34428b Mon Sep 17 00:00:00 2001 From: Robert Kaussow Date: Fri, 10 May 2024 08:42:59 +0200 Subject: [PATCH] refactor: switch to plugin Cmd and add tests (#110) --- .mockery.yaml | 6 + Makefile | 9 +- cmd/wp-matrix/docs.go | 58 ---------- cmd/wp-matrix/flags.go | 78 ------------- cmd/wp-matrix/main.go | 15 +-- cmd/wp-matrix/templates/docs-data.yaml.tmpl | 18 --- docs/data/data.yaml | 14 +++ go.mod | 13 ++- go.sum | 19 ++-- internal/doc/main.go | 42 +++++++ plugin/impl.go | 63 ++++------- plugin/impl_test.go | 41 +++---- plugin/matrix.go | 62 +++++++++++ plugin/matrix_test.go | 106 ++++++++++++++++++ plugin/mocks/mock_MautrixClient.go | 117 ++++++++++++++++++++ plugin/plugin.go | 99 ++++++++++++++++- plugin/util.go | 16 +++ plugin/util_test.go | 46 ++++++++ 18 files changed, 558 insertions(+), 264 deletions(-) create mode 100644 .mockery.yaml delete mode 100644 cmd/wp-matrix/docs.go delete mode 100644 cmd/wp-matrix/flags.go delete mode 100644 cmd/wp-matrix/templates/docs-data.yaml.tmpl create mode 100644 internal/doc/main.go create mode 100644 plugin/matrix.go create mode 100644 plugin/matrix_test.go create mode 100644 plugin/mocks/mock_MautrixClient.go create mode 100644 plugin/util.go create mode 100644 plugin/util_test.go diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 0000000..bc347d0 --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,6 @@ +--- +all: True +dir: mocks +outpkg: "mocks" +packages: + github.com/thegeeklab/wp-matrix/plugin: diff --git a/Makefile b/Makefile index 1b538e6..2a5a56b 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ IMPORT := github.com/thegeeklab/$(EXECUTABLE) GO ?= go CWD ?= $(shell pwd) -PACKAGES ?= $(shell go list ./...) +PACKAGES ?= $(shell go list ./... | grep -Ev 'mocks') SOURCES ?= $(shell find . -name "*.go" -type f) GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@$(GOFUMPT_PACKAGE_VERSION) @@ -19,7 +19,6 @@ GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@$(G XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GOTESTSUM_PACKAGE ?= gotest.tools/gotestsum@latest -GENERATE ?= XGO_VERSION := go-1.22.x XGO_TARGETS ?= linux/amd64,linux/arm-6,linux/arm-7,linux/arm64 @@ -65,11 +64,7 @@ lint: golangci-lint .PHONY: generate generate: - $(GO) generate $(GENERATE) - -.PHONY: generate-docs -generate-docs: - $(GO) generate ./cmd/$(EXECUTABLE)/flags.go + $(GO) generate $(PACKAGES) .PHONY: test test: diff --git a/cmd/wp-matrix/docs.go b/cmd/wp-matrix/docs.go deleted file mode 100644 index 9b47148..0000000 --- a/cmd/wp-matrix/docs.go +++ /dev/null @@ -1,58 +0,0 @@ -//go:build generate -// +build generate - -package main - -import ( - "bytes" - "embed" - "fmt" - "os" - "text/template" - - "github.com/thegeeklab/wp-matrix/plugin" - "github.com/thegeeklab/wp-plugin-go/docs" - wp "github.com/thegeeklab/wp-plugin-go/plugin" - wp_template "github.com/thegeeklab/wp-plugin-go/template" - "github.com/urfave/cli/v2" -) - -//go:embed templates/docs-data.yaml.tmpl -var yamlTemplate embed.FS - -func main() { - settings := &plugin.Settings{} - app := &cli.App{ - Flags: settingsFlags(settings, wp.FlagsPluginCategory), - } - - out, err := toYAML(app) - if err != nil { - panic(err) - } - - fi, err := os.Create("../../docs/data/data-raw.yaml") - if err != nil { - panic(err) - } - defer fi.Close() - if _, err := fi.WriteString(out); err != nil { - panic(err) - } -} - -func toYAML(app *cli.App) (string, error) { - var w bytes.Buffer - - yamlTmpl, err := template.New("docs").Funcs(wp_template.LoadFuncMap()).ParseFS(yamlTemplate, "templates/docs-data.yaml.tmpl") - if err != nil { - fmt.Println(yamlTmpl) - return "", err - } - - if err := yamlTmpl.ExecuteTemplate(&w, "docs-data.yaml.tmpl", docs.GetTemplateData(app)); err != nil { - return "", err - } - - return w.String(), nil -} diff --git a/cmd/wp-matrix/flags.go b/cmd/wp-matrix/flags.go deleted file mode 100644 index d5a7071..0000000 --- a/cmd/wp-matrix/flags.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2020, the Drone Plugins project authors. -// Copyright (c) 2021, Robert Kaussow - -// Use of this source code is governed by an Apache 2.0 license that can be -// found in the LICENSE file. - -package main - -import ( - "github.com/thegeeklab/wp-matrix/plugin" - "github.com/urfave/cli/v2" -) - -// settingsFlags has the cli.Flags for the plugin.Settings. -// -//go:generate go run docs.go flags.go -func settingsFlags(settings *plugin.Settings, category string) []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: "username", - EnvVars: []string{"PLUGIN_USERNAME", "MATRIX_USERNAME"}, - Usage: "authentication username", - Destination: &settings.Username, - Category: category, - }, - &cli.StringFlag{ - Name: "password", - EnvVars: []string{"PLUGIN_PASSWORD", "MATRIX_PASSWORD"}, - Usage: "authentication password", - Destination: &settings.Password, - Category: category, - }, - &cli.StringFlag{ - Name: "userid", - EnvVars: []string{"PLUGIN_USER_ID", "PLUGIN_USERID", "MATRIX_USER_ID", "MATRIX_USERID"}, - Usage: "authentication user ID", - Destination: &settings.UserID, - Category: category, - }, - &cli.StringFlag{ - Name: "accesstoken", - EnvVars: []string{"PLUGIN_ACCESS_TOKEN", "PLUGIN_ACCESSTOKEN", "MATRIX_ACCESS_TOKEN", "MATRIX_ACCESSTOKEN"}, - Usage: "authentication access token", - Destination: &settings.AccessToken, - Category: category, - }, - &cli.StringFlag{ - Name: "homeserver", - EnvVars: []string{"PLUGIN_HOMESERVER", "MATRIX_HOMESERVER"}, - Usage: "matrix home server url", - Value: "https://matrix.org", - Destination: &settings.Homeserver, - Category: category, - }, - &cli.StringFlag{ - Name: "roomid", - EnvVars: []string{"PLUGIN_ROOMID", "MATRIX_ROOMID"}, - Usage: "roomid to send messages to", - Destination: &settings.RoomID, - Category: category, - }, - &cli.StringFlag{ - Name: "template", - EnvVars: []string{"PLUGIN_TEMPLATE", "MATRIX_TEMPLATE"}, - Usage: "golang template for the message", - Value: plugin.DefaultMessageTemplate, - Destination: &settings.Template, - Category: category, - }, - &cli.BoolFlag{ - Name: "template-unsafe", - EnvVars: []string{"PLUGIN_TEMPLATE_UNSAFE", "MATRIX_TEMPLATE_UNSAFE"}, - Usage: "render raw HTML and potentially dangerous links in template", - Destination: &settings.TemplateUnsafe, - Category: category, - }, - } -} diff --git a/cmd/wp-matrix/main.go b/cmd/wp-matrix/main.go index a7b897e..4970bd7 100644 --- a/cmd/wp-matrix/main.go +++ b/cmd/wp-matrix/main.go @@ -7,11 +7,7 @@ package main import ( - "fmt" - "github.com/thegeeklab/wp-matrix/plugin" - - wp "github.com/thegeeklab/wp-plugin-go/plugin" ) //nolint:gochecknoglobals @@ -21,14 +17,5 @@ var ( ) func main() { - settings := &plugin.Settings{} - options := wp.Options{ - Name: "wp-matrix", - Description: "Send messages to a Matrix room", - Version: BuildVersion, - VersionMetadata: fmt.Sprintf("date=%s", BuildDate), - Flags: settingsFlags(settings, wp.FlagsPluginCategory), - } - - plugin.New(options, settings).Run() + plugin.New(nil, BuildVersion, BuildDate).Run() } diff --git a/cmd/wp-matrix/templates/docs-data.yaml.tmpl b/cmd/wp-matrix/templates/docs-data.yaml.tmpl deleted file mode 100644 index e453a95..0000000 --- a/cmd/wp-matrix/templates/docs-data.yaml.tmpl +++ /dev/null @@ -1,18 +0,0 @@ ---- -{{- if .GlobalArgs }} -properties: -{{- range $v := .GlobalArgs }} - - name: {{ $v.Name }} - {{- with $v.Description }} - description: | - {{ . | ToSentence }} - {{- end }} - {{- with $v.Type }} - type: {{ . }} - {{- end }} - {{- with $v.Default }} - defaultValue: {{ . }} - {{- end }} - required: {{ default false $v.Required }} -{{ end -}} -{{ end -}} diff --git a/docs/data/data.yaml b/docs/data/data.yaml index 15bd5e5..028e36a 100644 --- a/docs/data/data.yaml +++ b/docs/data/data.yaml @@ -13,6 +13,20 @@ properties: defaultValue: "https://matrix.org" required: false + - name: insecure_skip_verify + description: | + Skip SSL verification. + type: bool + defaultValue: false + required: false + + - name: log_level + description: | + Plugin log level. + type: string + defaultValue: "info" + required: false + - name: password description: | Authentication password. diff --git a/go.mod b/go.mod index 050bffe..d00765a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,8 @@ go 1.22 require ( github.com/microcosm-cc/bluemonday v1.0.26 github.com/rs/zerolog v1.32.0 - github.com/thegeeklab/wp-plugin-go v1.7.1 + github.com/stretchr/testify v1.9.0 + github.com/thegeeklab/wp-plugin-go/v2 v2.3.1 github.com/urfave/cli/v2 v2.27.2 maunium.net/go/mautrix v0.18.1 ) @@ -16,6 +17,7 @@ require ( github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/huandu/xstrings v1.3.3 // indirect @@ -25,9 +27,11 @@ require ( github.com/mattn/go-isatty v0.0.19 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/spf13/cast v1.4.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect @@ -35,9 +39,10 @@ require ( github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect github.com/yuin/goldmark v1.7.1 // indirect go.mau.fi/util v0.4.2 // indirect - golang.org/x/crypto v0.22.0 // indirect + golang.org/x/crypto v0.23.0 // indirect golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.19.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 042fea2..e89dd83 100644 --- a/go.sum +++ b/go.sum @@ -54,12 +54,14 @@ github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/thegeeklab/wp-plugin-go v1.7.1 h1:zfR/rfNPuyVhXJu1fsLfp4+Mz2pTf6WwW/mIqw9750I= -github.com/thegeeklab/wp-plugin-go v1.7.1/go.mod h1:Ixi5plt9tpFGTu6yc/Inm5DcDpp3xPTeohfr86gf2EU= +github.com/thegeeklab/wp-plugin-go/v2 v2.3.1 h1:ARwYgTPZ5iPsmOenmqcCf8TjiEe8wBOHKO7H/Xshe48= +github.com/thegeeklab/wp-plugin-go/v2 v2.3.1/go.mod h1:0t8M8txtEFiaB6RqLX8vLrxkqAo5FT5Hx7dztN592D4= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -81,8 +83,8 @@ go.mau.fi/util v0.4.2/go.mod h1:PlAVfUUcPyHPrwnvjkJM9UFcPE7qGPDJqk+Oufa1Gtw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -90,8 +92,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -103,8 +105,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -116,6 +118,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/doc/main.go b/internal/doc/main.go new file mode 100644 index 0000000..02c3dd9 --- /dev/null +++ b/internal/doc/main.go @@ -0,0 +1,42 @@ +//go:build generate +// +build generate + +package main + +import ( + "context" + "flag" + "net/http" + "os" + "time" + + "github.com/thegeeklab/wp-matrix/plugin" + "github.com/thegeeklab/wp-plugin-go/v2/docs" + "github.com/thegeeklab/wp-plugin-go/v2/template" +) + +func main() { + tmpl := "https://raw.githubusercontent.com/thegeeklab/woodpecker-plugins/main/templates/docs-data.yaml.tmpl" + client := http.Client{ + Timeout: 30 * time.Second, + } + + p := plugin.New(nil) + + out, err := template.Render(context.Background(), client, tmpl, docs.GetTemplateData(p.App)) + if err != nil { + panic(err) + } + + outputFile := flag.String("output", "", "Output file path") + flag.Parse() + + if *outputFile == "" { + panic("no output file specified") + } + + err = os.WriteFile(*outputFile, []byte(out), 0o644) + if err != nil { + panic(err) + } +} diff --git a/plugin/impl.go b/plugin/impl.go index 02464ef..4080255 100644 --- a/plugin/impl.go +++ b/plugin/impl.go @@ -10,15 +10,10 @@ import ( "context" "errors" "fmt" - "net/http" - "strings" - "github.com/microcosm-cc/bluemonday" "github.com/rs/zerolog/log" - "github.com/thegeeklab/wp-plugin-go/template" + "github.com/thegeeklab/wp-plugin-go/v2/template" "maunium.net/go/mautrix" - "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" ) @@ -49,15 +44,15 @@ func (p *Plugin) Validate() error { // Execute provides the implementation of the plugin. func (p *Plugin) Execute() error { - muid := id.NewUserID(prepend("@", p.Settings.UserID), p.Settings.Homeserver) + muid := id.NewUserID(EnsurePrefix("@", p.Settings.UserID), p.Settings.Homeserver) - client, err := mautrix.NewClient(p.Settings.Homeserver, muid, p.Settings.AccessToken) + matrix, err := mautrix.NewClient(p.Settings.Homeserver, muid, p.Settings.AccessToken) if err != nil { return fmt.Errorf("failed to initialize client: %w", err) } if p.Settings.UserID == "" || p.Settings.AccessToken == "" { - _, err := client.Login( + _, err := matrix.Login( p.Network.Context, &mautrix.ReqLogin{ Type: "m.login.password", @@ -74,53 +69,33 @@ func (p *Plugin) Execute() error { log.Info().Msg("logged in successfully") - joined, err := client.JoinRoom(p.Network.Context, prepend("!", p.Settings.RoomID), "", nil) + joinResp, err := matrix.JoinRoom(p.Network.Context, EnsurePrefix("!", p.Settings.RoomID), "", nil) if err != nil { return fmt.Errorf("failed to join room: %w", err) } - content, err := p.messageContent(p.Network.Context, *p.Network.Client) + msg, err := p.CreateMessage() if err != nil { - return fmt.Errorf("failed to render template: %w", err) + return fmt.Errorf("failed to create message: %w", err) } - if _, err := client.SendMessageEvent(p.Network.Context, joined.RoomID, event.EventMessage, content); err != nil { - return fmt.Errorf("failed to submit message: %w", err) + client := NewMatrixClient(matrix) + client.Message.Opt = MatrixMessageOpt{ + RoomID: joinResp.RoomID, + Message: msg, + TemplateUnsafe: p.Settings.TemplateUnsafe, } - log.Info().Msg("message sent successfully") - - return nil -} - -func (p *Plugin) messageContent(ctx context.Context, client http.Client) (event.MessageEventContent, error) { - message, err := template.RenderTrim(ctx, client, p.Settings.Template, p.Metadata) - if err != nil { - return event.MessageEventContent{}, err - } - - content := format.RenderMarkdown(message, true, p.Settings.TemplateUnsafe) - - safeBody := format.HTMLToMarkdown(bluemonday.UGCPolicy().Sanitize(content.FormattedBody)) - if content.Body != safeBody { - content.Body = safeBody + if err := client.Message.Send(p.Network.Context); err != nil { + return fmt.Errorf("failed to send message: %w", err) } - if content.FormattedBody != "" { - content.FormattedBody = bluemonday.UGCPolicy().Sanitize(content.FormattedBody) - } + log.Info().Msg("message sent successfully") - return content, nil + return nil } -func prepend(prefix, input string) string { - if strings.TrimSpace(input) == "" { - return input - } - - if strings.HasPrefix(input, prefix) { - return input - } - - return prefix + input +// CreateMessage generates a message string based on the plugin's template and metadata. +func (p *Plugin) CreateMessage() (string, error) { + return template.RenderTrim(p.Network.Context, *p.Network.Client, p.Settings.Template, p.Metadata) } diff --git a/plugin/impl_test.go b/plugin/impl_test.go index d24c956..9d8f444 100644 --- a/plugin/impl_test.go +++ b/plugin/impl_test.go @@ -5,7 +5,8 @@ import ( "net/http" "testing" - wp "github.com/thegeeklab/wp-plugin-go/plugin" + "github.com/stretchr/testify/assert" + wp "github.com/thegeeklab/wp-plugin-go/v2/plugin" ) func Test_messageContent(t *testing.T) { @@ -13,9 +14,7 @@ func Test_messageContent(t *testing.T) { tests := []struct { name string want string - unsafe bool template string - meta wp.Metadata }{ { name: "render default template", @@ -25,29 +24,15 @@ func Test_messageContent(t *testing.T) { { name: "render unsafe html template", want: "Status: **success**\nBuild: octocat/demo", - unsafe: true, - template: "Status: **{{ .Pipeline.Status }}**
Build: {{ .Repository.Slug }}", - }, - { - name: "render html xss template", - want: "Status: **success**\nBuild: octocat/demo", - unsafe: true, - template: "Status: **{{ .Pipeline.Status }}**
Build: {{ .Repository.Slug }}", - }, - { - name: "render markdown xss template", - want: "Status: **success**\nBuild: octocat/demo", - unsafe: true, - template: "Status: **{{ .Pipeline.Status }}**
Build: [{{ .Repository.Slug }}](javascript:alert(XSS1'))", + template: "Status: **{{ .Pipeline.Status }}**\nBuild: {{ .Repository.Slug }}", }, } - options := wp.Options{ - Name: "wp-matrix", - Execute: func(_ context.Context) error { return nil }, + p := New(func(_ context.Context) error { return nil }) + p.Network = wp.Network{ + Context: context.Background(), + Client: &http.Client{}, } - - p := New(options, &Settings{}) p.Metadata = wp.Metadata{ Curr: wp.Commit{ Branch: "main", @@ -67,12 +52,12 @@ func Test_messageContent(t *testing.T) { } for _, tt := range tests { - p.Settings.Template = tt.template - p.Settings.TemplateUnsafe = tt.unsafe - content, _ := p.messageContent(context.Background(), http.Client{}) + t.Run(tt.name, func(t *testing.T) { + p.Settings.Template = tt.template - if content.Body != tt.want { - t.Errorf("messageContent: %q got: %q, want: %q", tt.name, content.Body, tt.want) - } + content, err := p.CreateMessage() + assert.NoError(t, err) + assert.Equal(t, tt.want, content) + }) } } diff --git a/plugin/matrix.go b/plugin/matrix.go new file mode 100644 index 0000000..82b3a94 --- /dev/null +++ b/plugin/matrix.go @@ -0,0 +1,62 @@ +package plugin + +import ( + "context" + "fmt" + + "github.com/microcosm-cc/bluemonday" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" +) + +//nolint:lll +type MautrixClient interface { + SendMessageEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}, extra ...mautrix.ReqSendEvent) (resp *mautrix.RespSendEvent, err error) +} + +type MatrixClient struct { + client MautrixClient + Message *MatrixMessage +} + +type MatrixMessage struct { + client MautrixClient + Opt MatrixMessageOpt +} + +type MatrixMessageOpt struct { + RoomID id.RoomID + Message string + TemplateUnsafe bool +} + +// NewMatrixClient creates a new MatrixClient instance with the provided mautrix.Client. +func NewMatrixClient(client *mautrix.Client) *MatrixClient { + return &MatrixClient{ + client: client, + Message: &MatrixMessage{ + client: client, + Opt: MatrixMessageOpt{}, + }, + } +} + +// Send sends a message to the specified room. It sanitizes the message content +// to remove potentially unsafe HTML. +func (m *MatrixMessage) Send(ctx context.Context) error { + content := format.RenderMarkdown(m.Opt.Message, true, m.Opt.TemplateUnsafe) + + if content.FormattedBody != "" { + content.Body = format.HTMLToMarkdown(bluemonday.UGCPolicy().Sanitize(content.FormattedBody)) + content.FormattedBody = bluemonday.UGCPolicy().Sanitize(content.FormattedBody) + } + + _, err := m.client.SendMessageEvent(ctx, m.Opt.RoomID, event.EventMessage, content) + if err != nil { + return fmt.Errorf("failed to send message: %w", err) + } + + return nil +} diff --git a/plugin/matrix_test.go b/plugin/matrix_test.go new file mode 100644 index 0000000..3f41231 --- /dev/null +++ b/plugin/matrix_test.go @@ -0,0 +1,106 @@ +package plugin + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/thegeeklab/wp-matrix/plugin/mocks" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" +) + +func TestMatrixMessageSend(t *testing.T) { + tests := []struct { + name string + messageOpt MatrixMessageOpt + want event.MessageEventContent + wantErr bool + }{ + { + name: "plain text message", + messageOpt: MatrixMessageOpt{ + RoomID: "test-room", + Message: "hello world", + }, + want: event.MessageEventContent{ + MsgType: "m.text", + Body: "hello world", + }, + }, + { + name: "markdown message", + messageOpt: MatrixMessageOpt{ + RoomID: "test-room", + Message: "**hello world**", + }, + want: event.MessageEventContent{ + MsgType: "m.text", + Body: "**hello world**", + Format: "org.matrix.custom.html", + FormattedBody: "hello world", + }, + }, + { + name: "html message", + messageOpt: MatrixMessageOpt{ + RoomID: "test-room", + Message: "hello
world", + TemplateUnsafe: true, + }, + want: event.MessageEventContent{ + MsgType: "m.text", + Body: "hello\nworld", + Format: "org.matrix.custom.html", + FormattedBody: "hello
world", + }, + }, + { + name: "safe html message", + messageOpt: MatrixMessageOpt{ + RoomID: "test-room", + Message: "hello world", + TemplateUnsafe: false, + }, + want: event.MessageEventContent{ + MsgType: "m.text", + Body: "hello world", + Format: "org.matrix.custom.html", + FormattedBody: "hello world<script>alert('XSS')</script>", + }, + }, + { + name: "unsafe html message", + messageOpt: MatrixMessageOpt{ + RoomID: "test-room", + Message: "hello world", + TemplateUnsafe: true, + }, + want: event.MessageEventContent{ + MsgType: "m.text", + Body: "hello world", + Format: "org.matrix.custom.html", + FormattedBody: "hello world", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + mockClient := mocks.NewMockMautrixClient(t) + m := &MatrixMessage{ + Opt: tt.messageOpt, + client: mockClient, + } + + mockClient. + On("SendMessageEvent", mock.Anything, tt.messageOpt.RoomID, event.EventMessage, tt.want). + Return(&mautrix.RespSendEvent{}, nil) + + err := m.Send(ctx) + assert.Equal(t, tt.wantErr, err != nil) + }) + } +} diff --git a/plugin/mocks/mock_MautrixClient.go b/plugin/mocks/mock_MautrixClient.go new file mode 100644 index 0000000..b23a7f6 --- /dev/null +++ b/plugin/mocks/mock_MautrixClient.go @@ -0,0 +1,117 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + event "maunium.net/go/mautrix/event" + id "maunium.net/go/mautrix/id" + + mautrix "maunium.net/go/mautrix" + + mock "github.com/stretchr/testify/mock" +) + +// MockMautrixClient is an autogenerated mock type for the MautrixClient type +type MockMautrixClient struct { + mock.Mock +} + +type MockMautrixClient_Expecter struct { + mock *mock.Mock +} + +func (_m *MockMautrixClient) EXPECT() *MockMautrixClient_Expecter { + return &MockMautrixClient_Expecter{mock: &_m.Mock} +} + +// SendMessageEvent provides a mock function with given fields: ctx, roomID, eventType, contentJSON, extra +func (_m *MockMautrixClient) SendMessageEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}, extra ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error) { + _va := make([]interface{}, len(extra)) + for _i := range extra { + _va[_i] = extra[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, roomID, eventType, contentJSON) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for SendMessageEvent") + } + + var r0 *mautrix.RespSendEvent + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, id.RoomID, event.Type, interface{}, ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error)); ok { + return rf(ctx, roomID, eventType, contentJSON, extra...) + } + if rf, ok := ret.Get(0).(func(context.Context, id.RoomID, event.Type, interface{}, ...mautrix.ReqSendEvent) *mautrix.RespSendEvent); ok { + r0 = rf(ctx, roomID, eventType, contentJSON, extra...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*mautrix.RespSendEvent) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, id.RoomID, event.Type, interface{}, ...mautrix.ReqSendEvent) error); ok { + r1 = rf(ctx, roomID, eventType, contentJSON, extra...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockMautrixClient_SendMessageEvent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMessageEvent' +type MockMautrixClient_SendMessageEvent_Call struct { + *mock.Call +} + +// SendMessageEvent is a helper method to define mock.On call +// - ctx context.Context +// - roomID id.RoomID +// - eventType event.Type +// - contentJSON interface{} +// - extra ...mautrix.ReqSendEvent +func (_e *MockMautrixClient_Expecter) SendMessageEvent(ctx interface{}, roomID interface{}, eventType interface{}, contentJSON interface{}, extra ...interface{}) *MockMautrixClient_SendMessageEvent_Call { + return &MockMautrixClient_SendMessageEvent_Call{Call: _e.mock.On("SendMessageEvent", + append([]interface{}{ctx, roomID, eventType, contentJSON}, extra...)...)} +} + +func (_c *MockMautrixClient_SendMessageEvent_Call) Run(run func(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}, extra ...mautrix.ReqSendEvent)) *MockMautrixClient_SendMessageEvent_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]mautrix.ReqSendEvent, len(args)-4) + for i, a := range args[4:] { + if a != nil { + variadicArgs[i] = a.(mautrix.ReqSendEvent) + } + } + run(args[0].(context.Context), args[1].(id.RoomID), args[2].(event.Type), args[3].(interface{}), variadicArgs...) + }) + return _c +} + +func (_c *MockMautrixClient_SendMessageEvent_Call) Return(resp *mautrix.RespSendEvent, err error) *MockMautrixClient_SendMessageEvent_Call { + _c.Call.Return(resp, err) + return _c +} + +func (_c *MockMautrixClient_SendMessageEvent_Call) RunAndReturn(run func(context.Context, id.RoomID, event.Type, interface{}, ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error)) *MockMautrixClient_SendMessageEvent_Call { + _c.Call.Return(run) + return _c +} + +// NewMockMautrixClient creates a new instance of MockMautrixClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockMautrixClient(t interface { + mock.TestingT + Cleanup(func()) +}) *MockMautrixClient { + mock := &MockMautrixClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/plugin/plugin.go b/plugin/plugin.go index e5faaa2..4dbfabf 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -7,9 +7,15 @@ package plugin import ( - wp "github.com/thegeeklab/wp-plugin-go/plugin" + "fmt" + + wp "github.com/thegeeklab/wp-plugin-go/v2/plugin" + "github.com/urfave/cli/v2" ) +//go:generate mockery +//go:generate go run ../internal/doc/main.go -output=../docs/data/data-raw.yaml + //nolint:lll const DefaultMessageTemplate = ` Status: **{{ .Pipeline.Status }}** @@ -35,13 +41,96 @@ type Settings struct { TemplateUnsafe bool } -func New(options wp.Options, settings *Settings) *Plugin { - p := &Plugin{} +func New(e wp.ExecuteFunc, build ...string) *Plugin { + p := &Plugin{ + Settings: &Settings{}, + } + + options := wp.Options{ + Name: "wp-matrix", + Description: "Send messages to a Matrix room", + Flags: Flags(p.Settings, wp.FlagsPluginCategory), + Execute: p.run, + HideWoodpeckerFlags: true, + } + + if len(build) > 0 { + options.Version = build[0] + } - options.Execute = p.run + if len(build) > 1 { + options.VersionMetadata = fmt.Sprintf("date=%s", build[1]) + } + + if e != nil { + options.Execute = e + } p.Plugin = wp.New(options) - p.Settings = settings return p } + +// Flags returns a slice of CLI flags for the plugin. +func Flags(settings *Settings, category string) []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "username", + EnvVars: []string{"PLUGIN_USERNAME", "MATRIX_USERNAME"}, + Usage: "authentication username", + Destination: &settings.Username, + Category: category, + }, + &cli.StringFlag{ + Name: "password", + EnvVars: []string{"PLUGIN_PASSWORD", "MATRIX_PASSWORD"}, + Usage: "authentication password", + Destination: &settings.Password, + Category: category, + }, + &cli.StringFlag{ + Name: "userid", + EnvVars: []string{"PLUGIN_USER_ID", "PLUGIN_USERID", "MATRIX_USER_ID", "MATRIX_USERID"}, + Usage: "authentication user ID", + Destination: &settings.UserID, + Category: category, + }, + &cli.StringFlag{ + Name: "accesstoken", + EnvVars: []string{"PLUGIN_ACCESS_TOKEN", "PLUGIN_ACCESSTOKEN", "MATRIX_ACCESS_TOKEN", "MATRIX_ACCESSTOKEN"}, + Usage: "authentication access token", + Destination: &settings.AccessToken, + Category: category, + }, + &cli.StringFlag{ + Name: "homeserver", + EnvVars: []string{"PLUGIN_HOMESERVER", "MATRIX_HOMESERVER"}, + Usage: "matrix home server url", + Value: "https://matrix.org", + Destination: &settings.Homeserver, + Category: category, + }, + &cli.StringFlag{ + Name: "roomid", + EnvVars: []string{"PLUGIN_ROOMID", "MATRIX_ROOMID"}, + Usage: "roomid to send messages to", + Destination: &settings.RoomID, + Category: category, + }, + &cli.StringFlag{ + Name: "template", + EnvVars: []string{"PLUGIN_TEMPLATE", "MATRIX_TEMPLATE"}, + Usage: "golang template for the message", + Value: DefaultMessageTemplate, + Destination: &settings.Template, + Category: category, + }, + &cli.BoolFlag{ + Name: "template-unsafe", + EnvVars: []string{"PLUGIN_TEMPLATE_UNSAFE", "MATRIX_TEMPLATE_UNSAFE"}, + Usage: "render raw HTML and potentially dangerous links in template", + Destination: &settings.TemplateUnsafe, + Category: category, + }, + } +} diff --git a/plugin/util.go b/plugin/util.go new file mode 100644 index 0000000..9299b3d --- /dev/null +++ b/plugin/util.go @@ -0,0 +1,16 @@ +package plugin + +import "strings" + +// EnsurePrefix ensures that the given input string starts with the provided prefix. +func EnsurePrefix(prefix, input string) string { + if strings.TrimSpace(input) == "" { + return input + } + + if strings.HasPrefix(input, prefix) { + return input + } + + return prefix + input +} diff --git a/plugin/util_test.go b/plugin/util_test.go new file mode 100644 index 0000000..be1e7b8 --- /dev/null +++ b/plugin/util_test.go @@ -0,0 +1,46 @@ +package plugin + +import "testing" + +func TestEnsurePrefix(t *testing.T) { + tests := []struct { + name string + prefix string + input string + want string + }{ + { + name: "empty input", + prefix: "pre_", + input: "", + want: "", + }, + { + name: "input already has prefix", + prefix: "pre_", + input: "pre_value", + want: "pre_value", + }, + { + name: "input needs prefix", + prefix: "pre_", + input: "value", + want: "pre_value", + }, + { + name: "input with leading/trailing spaces", + prefix: "pre_", + input: " value ", + want: "pre_ value ", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := EnsurePrefix(tt.prefix, tt.input) + if got != tt.want { + t.Errorf("EnsurePrefix(%q, %q) = %q, want %q", tt.prefix, tt.input, got, tt.want) + } + }) + } +}