From 0560a893cfb3de4dcc30dbdcb94a5339f05107cd Mon Sep 17 00:00:00 2001 From: Dmitry Verkhoturov Date: Mon, 25 Apr 2022 21:32:42 +0200 Subject: [PATCH] switch to go-pkgz/notify package: modules --- backend/_example/memory_store/go.mod | 2 +- backend/_example/memory_store/go.sum | 4 +- backend/app/migrator/mapper.go | 4 +- backend/go.mod | 8 +- backend/go.sum | 12 +- .../github.com/go-pkgz/email/.gitignore | 15 + .../github.com/go-pkgz/email/.golangci.yml | 61 +++ .../vendor/github.com/go-pkgz/email/LICENSE | 21 + .../vendor/github.com/go-pkgz/email/README.md | 70 +++ .../vendor/github.com/go-pkgz/email/email.go | 347 +++++++++++++ .../github.com/go-pkgz/email/options.go | 70 +++ .../github.com/go-pkgz/notify/.golangci.yml | 70 +++ .../vendor/github.com/go-pkgz/notify/LICENSE | 21 + .../github.com/go-pkgz/notify/README.md | 202 ++++++++ .../vendor/github.com/go-pkgz/notify/email.go | 132 +++++ .../github.com/go-pkgz/notify/interface.go | 28 + .../vendor/github.com/go-pkgz/notify/slack.go | 108 ++++ .../github.com/go-pkgz/notify/telegram.go | 478 ++++++++++++++++++ .../github.com/go-pkgz/notify/webhook.go | 96 ++++ .../vendor/github.com/slack-go/slack/apps.go | 6 +- .../vendor/github.com/slack-go/slack/audit.go | 10 + .../slack-go/slack/block_element.go | 6 + .../github.com/slack-go/slack/block_object.go | 3 +- .../vendor/github.com/slack-go/slack/chat.go | 60 ++- .../vendor/github.com/slack-go/slack/files.go | 79 +-- .../vendor/github.com/slack-go/slack/info.go | 6 +- .../github.com/slack-go/slack/interactions.go | 54 +- .../github.com/slack-go/slack/messageID.go | 20 +- .../vendor/github.com/slack-go/slack/misc.go | 26 +- .../github.com/slack-go/slack/reminders.go | 36 +- .../slack-go/slack/slackutilsx/slackutilsx.go | 6 +- .../vendor/github.com/slack-go/slack/views.go | 2 +- .../github.com/slack-go/slack/webhooks.go | 24 + .../slack-go/slack/webhooks_go112.go | 34 -- .../slack-go/slack/webhooks_go113.go | 33 -- .../slack-go/slack/websocket_managed_conn.go | 6 +- .../slack-go/slack/workflow_step.go | 98 ++++ backend/vendor/modules.txt | 10 +- 38 files changed, 2061 insertions(+), 207 deletions(-) create mode 100644 backend/vendor/github.com/go-pkgz/email/.gitignore create mode 100644 backend/vendor/github.com/go-pkgz/email/.golangci.yml create mode 100644 backend/vendor/github.com/go-pkgz/email/LICENSE create mode 100644 backend/vendor/github.com/go-pkgz/email/README.md create mode 100644 backend/vendor/github.com/go-pkgz/email/email.go create mode 100644 backend/vendor/github.com/go-pkgz/email/options.go create mode 100644 backend/vendor/github.com/go-pkgz/notify/.golangci.yml create mode 100644 backend/vendor/github.com/go-pkgz/notify/LICENSE create mode 100644 backend/vendor/github.com/go-pkgz/notify/README.md create mode 100644 backend/vendor/github.com/go-pkgz/notify/email.go create mode 100644 backend/vendor/github.com/go-pkgz/notify/interface.go create mode 100644 backend/vendor/github.com/go-pkgz/notify/slack.go create mode 100644 backend/vendor/github.com/go-pkgz/notify/telegram.go create mode 100644 backend/vendor/github.com/go-pkgz/notify/webhook.go delete mode 100644 backend/vendor/github.com/slack-go/slack/webhooks_go112.go delete mode 100644 backend/vendor/github.com/slack-go/slack/webhooks_go113.go create mode 100644 backend/vendor/github.com/slack-go/slack/workflow_step.go diff --git a/backend/_example/memory_store/go.mod b/backend/_example/memory_store/go.mod index bfbdb519..1ddc6493 100644 --- a/backend/_example/memory_store/go.mod +++ b/backend/_example/memory_store/go.mod @@ -35,7 +35,7 @@ require ( github.com/stretchr/objx v0.3.0 // indirect go.etcd.io/bbolt v1.3.6 // indirect golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 // indirect - golang.org/x/net v0.0.0-20220412020605-290c469a71a5 // indirect + golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect diff --git a/backend/_example/memory_store/go.sum b/backend/_example/memory_store/go.sum index 3f14182e..4cc680ce 100644 --- a/backend/_example/memory_store/go.sum +++ b/backend/_example/memory_store/go.sum @@ -88,8 +88,8 @@ golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeap golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/backend/app/migrator/mapper.go b/backend/app/migrator/mapper.go index 901b3e28..7d215ff9 100644 --- a/backend/app/migrator/mapper.go +++ b/backend/app/migrator/mapper.go @@ -1,7 +1,7 @@ package migrator import ( - "errors" + "fmt" "io" "strings" ) @@ -42,7 +42,7 @@ func (u *URLMapper) loadRules(reader io.Reader) error { row = strings.TrimSpace(row) urls := strings.Split(row, " ") if len(urls) != 2 { - return errors.New("bad row " + row) + return fmt.Errorf("bad row %s", row) } from, to := strings.TrimSpace(urls[0]), strings.TrimSpace(urls[1]) diff --git a/backend/go.mod b/backend/go.mod index 52845773..1ac1aaca 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -15,6 +15,7 @@ require ( github.com/go-pkgz/jrpc v0.2.0 github.com/go-pkgz/lcw v0.8.1 github.com/go-pkgz/lgr v0.10.4 + github.com/go-pkgz/notify v0.0.0-20220429105545-bc5c80a54199 github.com/go-pkgz/repeater v1.1.3 github.com/go-pkgz/rest v1.14.0 github.com/go-pkgz/syncs v1.2.0 @@ -25,18 +26,16 @@ require ( github.com/jessevdk/go-flags v1.5.0 github.com/kyokomi/emoji/v2 v2.2.9 github.com/microcosm-cc/bluemonday v1.0.18 - github.com/pkg/errors v0.9.1 github.com/rakyll/statik v0.1.7 github.com/rs/xid v1.4.0 github.com/russross/blackfriday/v2 v2.1.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e - github.com/slack-go/slack v0.10.2 github.com/stretchr/testify v1.7.1 go.etcd.io/bbolt v1.3.6 go.uber.org/goleak v1.1.12 golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 - golang.org/x/net v0.0.0-20220412020605-290c469a71a5 + golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 ) require ( @@ -48,6 +47,7 @@ require ( github.com/dlclark/regexp2 v1.4.0 // indirect github.com/go-chi/chi v4.1.1+incompatible // indirect github.com/go-oauth2/oauth2/v4 v4.4.3 // indirect + github.com/go-pkgz/email v0.2.1-0.20220428155644-a7e5d837d02f // indirect github.com/go-pkgz/expirable-cache v0.0.3 // indirect github.com/go-redis/redis/v7 v7.4.1 // indirect github.com/go-stack/stack v1.8.1 // indirect @@ -59,7 +59,9 @@ require ( github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/klauspost/compress v1.15.1 // indirect github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/slack-go/slack v0.10.3 // indirect github.com/stretchr/objx v0.3.0 // indirect github.com/valyala/fasthttp v1.35.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index fbdfa393..eae65014 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -144,6 +144,8 @@ github.com/go-oauth2/oauth2/v4 v4.4.3 h1:IiU3iAK5NEeZ4mUf5HV9tN7iNlx4AGUkIJ5M1C4 github.com/go-oauth2/oauth2/v4 v4.4.3/go.mod h1:NR9Hugz5/Qe2OGxoPBhsTRNjnm/amC+z9+XTwt63rhs= github.com/go-pkgz/auth v1.19.0 h1:TTfbQvlqsuEhRpYAiY/uF5UGP+HWMc5CN2RAhN06ik0= github.com/go-pkgz/auth v1.19.0/go.mod h1:1pu95rx7tFfeIFq0pjQPwCeL6qGYH4+UxHJudhK0T7M= +github.com/go-pkgz/email v0.2.1-0.20220428155644-a7e5d837d02f h1:beMw0mHR3m7yh0tl2CwRTAg2Pa9z2LGFzHNDiD93i2Y= +github.com/go-pkgz/email v0.2.1-0.20220428155644-a7e5d837d02f/go.mod h1:TpnmSLkQW3FyICit2hn7WIhCUDrhCX6btzz5wS3wHRI= github.com/go-pkgz/expirable-cache v0.0.3 h1:rTh6qNPp78z0bQE6HDhXBHUwqnV9i09Vm6dksJLXQDc= github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ= github.com/go-pkgz/jrpc v0.2.0 h1:CLy/eZyekjraVrxZV18N2R1mYLMJ/nWrgdfyIOGPY/E= @@ -152,6 +154,8 @@ github.com/go-pkgz/lcw v0.8.1 h1:Bpt2yYTE1J8hIhz8tjdm1WPOgH13eo5iTNsXyop7cMQ= github.com/go-pkgz/lcw v0.8.1/go.mod h1:Xw0/ZfApATgbjVPYRZO4XHdWyxAjErDWDWJ7TLlw1Vc= github.com/go-pkgz/lgr v0.10.4 h1:l7qyFjqEZgwRgaQQSEp6tve4A3OU80VrfzpvtEX8ngw= github.com/go-pkgz/lgr v0.10.4/go.mod h1:CD0s1z6EFpIUplV067gitF77tn25JItzwHNKAPqeCF0= +github.com/go-pkgz/notify v0.0.0-20220429105545-bc5c80a54199 h1:1PE3B3RboNvAVL3V6b+GXNFHaTiIcTohsqh+HbUkpIg= +github.com/go-pkgz/notify v0.0.0-20220429105545-bc5c80a54199/go.mod h1:mSHXWImQev182O0gte/r6pT7NEzlTZozcOhOykjnVqk= github.com/go-pkgz/repeater v1.1.3 h1:q6+JQF14ESSy28Dd7F+wRelY4F+41HJ0LEy/szNnMiE= github.com/go-pkgz/repeater v1.1.3/go.mod h1:hVTavuO5x3Gxnu8zW7d6sQBfAneKV8X2FjU48kGfpKw= github.com/go-pkgz/rest v1.5.0/go.mod h1:nQaM3RhSTUAmbBZWY4hfe4buyeC9VckvhoCktiQXJxI= @@ -343,8 +347,8 @@ github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNX github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= -github.com/slack-go/slack v0.10.2 h1:KMN/h2sgUninHXvQI8PrR/PHBUuWp2NPvz2Kr66tki4= -github.com/slack-go/slack v0.10.2/go.mod h1:5FLdBRv7VW/d9EBxx/eEktOptWygbA9K2QK/KW7ds1s= +github.com/slack-go/slack v0.10.3 h1:kKYwlKY73AfSrtAk9UHWCXXfitudkDztNI9GYBviLxw= +github.com/slack-go/slack v0.10.3/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -526,8 +530,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/backend/vendor/github.com/go-pkgz/email/.gitignore b/backend/vendor/github.com/go-pkgz/email/.gitignore new file mode 100644 index 00000000..66fd13c9 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/email/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/backend/vendor/github.com/go-pkgz/email/.golangci.yml b/backend/vendor/github.com/go-pkgz/email/.golangci.yml new file mode 100644 index 00000000..63d9790b --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/email/.golangci.yml @@ -0,0 +1,61 @@ +run: + timeout: 5m + output: + format: tab + skip-dirs: + - vendor + +linters-settings: + govet: + check-shadowing: true + maligned: + suggest-new: true + goconst: + min-len: 2 + min-occurrences: 2 + misspell: + locale: US + lll: + line-length: 140 + gocritic: + enabled-tags: + - performance + - style + - experimental + disabled-checks: + - wrapperFunc + - hugeParam + - rangeValCopy + - singleCaseSwitch + - ifElseChain + +linters: + enable: + - megacheck + - revive + - govet + - unconvert + - megacheck + - structcheck + - gas + - gocyclo + - dupl + - misspell + - unparam + - varcheck + - deadcode + - typecheck + - ineffassign + - varcheck + - stylecheck + - gochecknoinits + - exportloopref + - gocritic + - nakedret + - gosimple + - prealloc + fast: false + disable-all: true + +issues: + exclude-use-default: false diff --git a/backend/vendor/github.com/go-pkgz/email/LICENSE b/backend/vendor/github.com/go-pkgz/email/LICENSE new file mode 100644 index 00000000..4203bbbb --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/email/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Umputun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/vendor/github.com/go-pkgz/email/README.md b/backend/vendor/github.com/go-pkgz/email/README.md new file mode 100644 index 00000000..78d894f9 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/email/README.md @@ -0,0 +1,70 @@ +# email sending library + +[![Build Status](https://github.com/go-pkgz/email/workflows/build/badge.svg)](https://github.com/go-pkgz/email/actions) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/email/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/email?branch=master) [![Go Reference](https://pkg.go.dev/badge/github.com/go-pkgz/email.svg)](https://pkg.go.dev/github.com/go-pkgz/email) + + +The library is a wrapper around the stdlib `net/smtp` simplifying email sending. It supports authentication, SSL/TLS, +user-specified SMTP servers, content-type, charset, multiple recipients and more. + +Usage example: + +```go +client := email.NewSender("localhost", email.ContentType("text/html"), email.Auth("user", "pass")) +err := client.Send("some content, foo bar", + email.Params{From: "me@example.com", To: []string{"to@example.com"}, Subject: "Hello world!", + Attachments: []string{"/path/to/file1.txt", "/path/to/file2.txt"}, + InlineImages: []string{"/path/to/image1.png", "/path/to/image2.png"}, + }) +``` + +## options + +`NewSender` accepts a number of options to configure the client: + +- `Port`: SMTP port (default: 25) +- `TLS`: Use TLS SMTP (default: false) +- `STARTTLS`: Use STARTTLS (default: false) +- `Auth(user, password)`: Username and password for SMTP authentication (default: empty, no authentication) +- `ContentType`: Content type for the email (default: "text/plain") +- `Charset`: Charset for the email (default: "utf-8") +- `TimeOut`: Timeout for the SMTP connection (default: 30 seconds) +- `Log`: Logger to use (default: no logging) +- `SMTP`: Set custom smtp client (default: none) + +See [go docs](https://pkg.go.dev/github.com/go-pkgz/email#Option) for `Option` functions. + +_Options should be passed to `NewSender` after the mandatory first (host) parameter._ + +## sending email + +To send email user need to create a sender first and then use `Send` method. The method accepts two parameters: + +- email content (string) +- parameters (`email.Params`) + ```go + type Params struct { + From string // From email field + To []string // From email field + Subject string // Email subject + UnsubscribeLink string // POST, https://support.google.com/mail/answer/81126 -> "Use one-click unsubscribe" + Attachments []string // Attachments path + InlineImages []string // Embedding directly to email body. Autogenerated Content-Id (cid) equals to file name + } + ``` + +See [go docs](https://pkg.go.dev/github.com/go-pkgz/email#Sender.Send) for `Send` function. + +## technical details + +- Content-Transfer-Encoding set to `quoted-printable` +- Custom SMTP client (`smtp.Client` from stdlib) can be set by user with `SMTP` option. In this case it will be used instead of making a new smtp client internally. +- Logger can be set with `Log` option. It should implement `email.Logger` interface with a single `Logf(format string, args ...interface{})` method. By default, "no logging" internal logger is used. This interface is compatible with the `go-pkgz/lgr` logger. +- The library has no external dependencies, except for testing. It uses the stdlib `net/smtp` package. +- SSL/TLS supported with `TLS` option. Pls note: this is not the same as `STARTTLS` (not supported) which is usually on port 587 vs SSL/TLS on port 465. + +## limitations + +This library is not intended to be used for sending a lot of massive emails with +low latency requirements. The intended use case is sending simple messages, like alerts, notification and so on. +For example, sending alerts from a monitoring system, or for authentication-related emails, i.e. "password reset email", +"verification email", etc. diff --git a/backend/vendor/github.com/go-pkgz/email/email.go b/backend/vendor/github.com/go-pkgz/email/email.go new file mode 100644 index 00000000..2b345c36 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/email/email.go @@ -0,0 +1,347 @@ +// Package email provides email sender +package email + +import ( + "bytes" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "io" + "mime" + "mime/multipart" + "mime/quotedprintable" + "net" + "net/http" + "net/smtp" + "net/textproto" + "os" + "path/filepath" + "strings" + "time" +) + +//go:generate moq -out mocks/smpt_client.go -pkg mocks -skip-ensure -fmt goimports . SMTPClient +//go:generate moq -out mocks/logger.go -pkg mocks -skip-ensure -fmt goimports . Logger + +// Sender implements email sender +type Sender struct { + smtpClient SMTPClient + logger Logger + host string // SMTP host + port int // SMTP port + contentType string // Content type, optional. Will trigger MIME and Content-Type headers + tls bool // TLS auth + starttls bool // StartTLS + smtpUserName string // username + smtpPassword string // password + timeOut time.Duration + contentCharset string + timeNow func() time.Time +} + +// Params contains all user-defined parameters to send emails +type Params struct { + From string // From email field + To []string // From email field + Subject string // Email subject + UnsubscribeLink string // POST, https://support.google.com/mail/answer/81126 -> "Use one-click unsubscribe" + Attachments []string // Attachments path + InlineImages []string // InlineImages images path +} + +// Logger is used to log errors and debug messages +type Logger interface { + Logf(format string, args ...interface{}) +} + +// SMTPClient interface defines subset of net/smtp used by email client +type SMTPClient interface { + Mail(from string) error + Auth(auth smtp.Auth) error + Rcpt(to string) error + Data() (io.WriteCloser, error) + Quit() error + Close() error +} + +// NewSender creates email client with prepared smtp +func NewSender(smtpHost string, options ...Option) *Sender { + res := Sender{ + smtpClient: nil, + logger: nopLogger{}, + host: smtpHost, + port: 25, + contentType: `text/plain`, + tls: false, + smtpUserName: "", + smtpPassword: "", + contentCharset: "UTF-8", + timeOut: time.Second * 30, + timeNow: time.Now, + } + for _, opt := range options { + opt(&res) + } + + res.logger.Logf("[INFO] new email sender created with host: %s:%d, tls: %v, username: %q, timeout: %v, "+ + "content type: %q, charset: %q", smtpHost, + res.port, res.tls, res.smtpUserName, res.timeOut, res.contentType, res.contentCharset) + return &res +} + +// Send email with given text +// If SMTPClient defined in Email struct it will be used, if not - new smtp.Client on each send. +// Always closes client on completion or failure. +func (em *Sender) Send(text string, params Params) error { + em.logger.Logf("[DEBUG] send %q to %v", text, params.To) + + client := em.smtpClient + if client == nil { // if client not set make new net/smtp + c, err := em.client() + if err != nil { + return fmt.Errorf("failed to make smtp client: %w", err) + } + client = c + } + + var quit bool + defer func() { + if quit || client == nil { // quit set if Quit() call passed because it's closing connection as well. + return + } + if err := client.Close(); err != nil { + em.logger.Logf("[WARN] can't close smtp connection, %v", err) + } + }() + + if len(params.To) == 0 { + return errors.New("no recipients") + } + + if em.smtpUserName != "" && em.smtpPassword != "" { + auth := smtp.PlainAuth("", em.smtpUserName, em.smtpPassword, em.host) + if err := client.Auth(auth); err != nil { + return fmt.Errorf("failed to auth to smtp %s:%d, %w", em.host, em.port, err) + } + } + + if err := client.Mail(params.From); err != nil { + return fmt.Errorf("bad from address %q: %w", params.From, err) + } + + for _, rcpt := range params.To { + if err := client.Rcpt(rcpt); err != nil { + return fmt.Errorf("bad to address %q: %w", params.To, err) + } + } + + writer, err := client.Data() + if err != nil { + return fmt.Errorf("can't make email writer: %w", err) + } + + msg, err := em.buildMessage(text, params) + if err != nil { + return fmt.Errorf("can't make email message: %w", err) + } + buf := bytes.NewBufferString(msg) + if _, err = buf.WriteTo(writer); err != nil { + return fmt.Errorf("failed to send email body to %q: %w", params.To, err) + } + if err = writer.Close(); err != nil { + em.logger.Logf("[WARN] can't close smtp body writer, %v", err) + } + + if err = client.Quit(); err != nil { + em.logger.Logf("[WARN] failed to send quit command to %s:%d, %v", em.host, em.port, err) + } else { + quit = true + } + return nil +} + +func (em *Sender) String() string { + return fmt.Sprintf("smtp://%s:%d, auth:%v, tls:%v, starttls:%v, timeout:%v, content-type:%q, charset:%q", + em.host, em.port, em.smtpUserName != "", em.tls, em.starttls, em.timeOut, em.contentType, em.contentCharset) +} + +func (em *Sender) client() (c *smtp.Client, err error) { + srvAddress := fmt.Sprintf("%s:%d", em.host, em.port) + tlsConf := &tls.Config{ + InsecureSkipVerify: false, + ServerName: em.host, + MinVersion: tls.VersionTLS12, + } + + if em.tls { + conn, e := tls.Dial("tcp", srvAddress, tlsConf) + if e != nil { + return nil, fmt.Errorf("failed to dial smtp tls to %s: %w", srvAddress, e) + } + if c, err = smtp.NewClient(conn, em.host); err != nil { + return nil, fmt.Errorf("failed to make smtp client for %s: %w", srvAddress, err) + } + return c, nil + } + + conn, err := net.DialTimeout("tcp", srvAddress, em.timeOut) + if err != nil { + return nil, fmt.Errorf("timeout connecting to %s: %w", srvAddress, err) + } + + c, err = smtp.NewClient(conn, srvAddress) + if err != nil { + return nil, fmt.Errorf("failed to dial: %w", err) + } + + if em.starttls { + if err = c.StartTLS(tlsConf); err != nil { + return nil, fmt.Errorf("failed to start tls: %w", err) + } + } + + return c, nil +} + +func (em *Sender) buildMessage(text string, params Params) (message string, err error) { + addHeader := func(msg, h, v string) string { + msg += fmt.Sprintf("%s: %s\n", h, v) + return msg + } + message = addHeader(message, "From", params.From) + message = addHeader(message, "To", strings.Join(params.To, ",")) + message = addHeader(message, "Subject", mime.BEncoding.Encode("utf-8", params.Subject)) + + if params.UnsubscribeLink != "" { + message = addHeader(message, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click") + message = addHeader(message, "List-Unsubscribe", "<"+params.UnsubscribeLink+">") + } + + withAttachments := len(params.Attachments) > 0 + withInlineImg := len(params.InlineImages) > 0 + + if em.contentType != "" || withAttachments || withInlineImg { + message = addHeader(message, "MIME-version", "1.0") + } + + message = addHeader(message, "Date", em.timeNow().Format(time.RFC1123Z)) + + buff := &bytes.Buffer{} + qp := quotedprintable.NewWriter(buff) + mpMixed := multipart.NewWriter(buff) + boundaryMixed := mpMixed.Boundary() + mpRelated := multipart.NewWriter(buff) + boundaryRelated := mpRelated.Boundary() + + if withAttachments { + message = addHeader(message, "Content-Type", fmt.Sprintf("multipart/mixed; boundary=%q\r\n\r\n%s\r", + boundaryMixed, "--"+boundaryMixed)) + } + + if withInlineImg { + message = addHeader(message, "Content-Type", fmt.Sprintf("multipart/related; boundary=%q\r\n\r\n%s\r", + boundaryRelated, "--"+boundaryRelated)) + } + + if em.contentType != "" { + message = addHeader(message, "Content-Transfer-Encoding", "quoted-printable") + message = addHeader(message, "Content-Type", fmt.Sprintf("%s; charset=%q", em.contentType, em.contentCharset)) + + } + + if err := em.writeBody(qp, text); err != nil { + return "", fmt.Errorf("failed to write body: %w", err) + } + + if withInlineImg { + buff.WriteString("\r\n\r\n") + if err := em.writeFiles(mpRelated, params.InlineImages, "inline"); err != nil { + return "", fmt.Errorf("failed to write inline images: %w", err) + } + } + + if withAttachments { + buff.WriteString("\r\n\r\n") + if err := em.writeFiles(mpMixed, params.Attachments, "attachment"); err != nil { + return "", fmt.Errorf("failed to write attachments: %w", err) + } + } + + m := buff.String() + message += "\n" + m + // returns base part of the file location + return message, nil +} + +func (em *Sender) writeBody(wc io.WriteCloser, text string) error { + if _, err := wc.Write([]byte(text)); err != nil { + return err + } + if err := wc.Close(); err != nil { + return err + } + return nil +} + +func (em *Sender) writeFiles(mp *multipart.Writer, files []string, disposition string) error { + for _, attachment := range files { + file, err := os.Open(filepath.Clean(attachment)) + if err != nil { + return err + } + + // we need first 512 bytes to detect file type + fTypeBuff := make([]byte, 512) + _, err = file.Read(fTypeBuff) + if err != nil { + return fmt.Errorf("failed to read file type %q: %w", attachment, err) + } + + // remove null bytes in case file less than 512 bytes + fTypeBuff = bytes.Trim(fTypeBuff, "\x00") + fName := filepath.Base(attachment) + header := textproto.MIMEHeader{} + header.Set("Content-Type", http.DetectContentType(fTypeBuff)+"; name=\""+fName+"\"") + header.Set("Content-Transfer-Encoding", "base64") + + switch disposition { + case "attachment": + header.Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", fName)) + case "inline": + header.Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", fName)) + header.Set("Content-ID", fmt.Sprintf("<%s>", fName)) + } + + writer, err := mp.CreatePart(header) + if err != nil { + return err + } + + // set reader offset at the beginning of the file because we read first 512 bytes + _, err = file.Seek(0, io.SeekStart) + if err != nil { + return err + } + + encoder := base64.NewEncoder(base64.StdEncoding, writer) + if _, err := io.Copy(encoder, file); err != nil { + return err + } + if err := encoder.Close(); err != nil { + return err + } + + if err := file.Close(); err != nil { + return err + } + } + if err := mp.Close(); err != nil { + return err + } + return nil +} + +type nopLogger struct{} + +func (nopLogger) Logf(format string, args ...interface{}) {} diff --git a/backend/vendor/github.com/go-pkgz/email/options.go b/backend/vendor/github.com/go-pkgz/email/options.go new file mode 100644 index 00000000..e1d210af --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/email/options.go @@ -0,0 +1,70 @@ +package email + +import "time" + +// Option func type +type Option func(s *Sender) + +// SMTP sets SMTP client +func SMTP(smtp SMTPClient) Option { + return func(s *Sender) { + s.smtpClient = smtp + } +} + +// Log sets the logger for the email package +func Log(l Logger) Option { + return func(s *Sender) { + s.logger = l + } +} + +// Port sets SMTP port +func Port(port int) Option { + return func(s *Sender) { + s.port = port + } +} + +// ContentType sets content type of the email +func ContentType(contentType string) Option { + return func(s *Sender) { + s.contentType = contentType + } +} + +// Charset sets content charset of the email +func Charset(charset string) Option { + return func(s *Sender) { + s.contentCharset = charset + } +} + +// TLS enables TLS support +func TLS(enabled bool) Option { + return func(s *Sender) { + s.tls = enabled + } +} + +// STARTTLS enables STARTTLS support +func STARTTLS(enabled bool) Option { + return func(s *Sender) { + s.starttls = enabled + } +} + +// Auth sets smtp username and password +func Auth(smtpUserName, smtpPasswd string) Option { + return func(s *Sender) { + s.smtpUserName = smtpUserName + s.smtpPassword = smtpPasswd + } +} + +// TimeOut sets smtp timeout +func TimeOut(timeOut time.Duration) Option { + return func(s *Sender) { + s.timeOut = timeOut + } +} diff --git a/backend/vendor/github.com/go-pkgz/notify/.golangci.yml b/backend/vendor/github.com/go-pkgz/notify/.golangci.yml new file mode 100644 index 00000000..bd7524ef --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/notify/.golangci.yml @@ -0,0 +1,70 @@ +run: + timeout: 5m + output: + format: tab + skip-dirs: + - vendor + +linters-settings: + govet: + check-shadowing: true + maligned: + suggest-new: true + goconst: + min-len: 2 + min-occurrences: 2 + misspell: + locale: US + lll: + line-length: 140 + gocritic: + enabled-tags: + - performance + - style + - experimental + disabled-checks: + - wrapperFunc + - hugeParam + - rangeValCopy + - singleCaseSwitch + - ifElseChain + +linters: + enable: + - megacheck + - revive + - govet + - unconvert + - megacheck + - structcheck + - gas + - gocyclo + - dupl + - misspell + - unparam + - varcheck + - deadcode + - typecheck + - ineffassign + - varcheck + - stylecheck + - gochecknoinits + - exportloopref + - gocritic + - nakedret + - gosimple + - prealloc + - whitespace + fast: false + disable-all: true + +issues: + exclude-rules: + - text: "at least one file in a package should have a package comment" + linters: + - stylecheck + - path: _test\.go + linters: + - gosec + - dupl + exclude-use-default: false diff --git a/backend/vendor/github.com/go-pkgz/notify/LICENSE b/backend/vendor/github.com/go-pkgz/notify/LICENSE new file mode 100644 index 00000000..682ea0c0 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/notify/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Umputun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/vendor/github.com/go-pkgz/notify/README.md b/backend/vendor/github.com/go-pkgz/notify/README.md new file mode 100644 index 00000000..ae995fae --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/notify/README.md @@ -0,0 +1,202 @@ +# Notify + +[![Build Status](https://github.com/go-pkgz/notify/workflows/build/badge.svg)](https://github.com/go-pkgz/notify/actions) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/notify/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/notify?branch=master) [![Go Reference](https://pkg.go.dev/badge/github.com/go-pkgz/notify.svg)](https://pkg.go.dev/github.com/go-pkgz/notify) + +This library provides ability to send notifications using multiple services: + +- Email +- Telegram +- Slack +- Webhook + +## Install + +`go get -u github.com/go-pkgz/notify` + +## Usage + +All supported notification methods could adhere to the following interface. Example on how to use it: + +```go +package main + +import ( + "context" + "fmt" + + "github.com/go-pkgz/notify" +) + +func main() { + // create notifiers + notifiers := []notify.Notifier{ + notify.NewWebhook(notify.WebhookParams{}), + notify.NewEmail(notify.SMTPParams{}), + notify.NewSlack("token"), + notify.NewTelegram(notify.TelegramParams{token: "token"}), + } + err := notify.Send(context.Background(), notifiers, "https://example.com/webhook", "Hello, world!") + if err != nil { + fmt.Printf("Sent message error: %s", err)) + } +} +``` + +### Email + +`mailto:` [scheme](https://datatracker.ietf.org/doc/html/rfc6068) is supported. Only `subject` and `from` query params are used. + +Examples: + +- `mailto:"John Wayne"?subject=test-subj&from="Notifier"` +- `mailto:addr1@example.org,addr2@example.org?&subject=test-subj&from=notify@example.org` + +```go +package main + +import ( + "context" + "log" + "time" + + "github.com/go-pkgz/notify" +) + +func main() { + wh := notify.NewEmail(notify.SMTPParams{ + Host: "localhost", // the only required field, others are optional + Port: 25, + TLS: false, // TLS, but not STARTTLS + ContentType: "text/html", + Charset: "UTF-8", + Username: "username", + Password: "password", + TimeOut: time.Second * 10, // default is 30 seconds + }) + err := wh.Send( + context.Background(), + `mailto:"John Wayne"?subject=test-subj&from="Notifier"`, + "Hello, World!", + ) + if err != nil { + log.Fatalf("problem sending message using email, %v", err) + } +} +``` + +### Telegram + +`telegram:` scheme akin to `mailto:` is supported. Query params `parseMode` ([doc](https://core.telegram.org/bots/api#formatting-options), legacy `Markdown` by default, preferable use `MarkdownV2` or `HTML` instead). Examples: + +- `telegram:channel` +- `telegram:channelID` // channel ID is a number, like `-1001480738202`: use [that instruction](https://remark42.com/docs/configuration/telegram/#notifications-for-administrators) to obtain it +- `telegram:userID` + +[Here](https://remark42.com/docs/configuration/telegram/#getting-bot-token-for-telegram) is an instruction on obtaining token for your notification bot. + +```go +package main + +import ( + "context" + "log" + + "github.com/go-pkgz/notify" +) + +func main() { + tg := notify.NewTelegram(notify.TelegramParams{ + Token: "token", // required + Timeout: time.Second * 10, // default is 5 seconds + SuccessMsg: // optional, for auth, set by default + ErrorMsg: // optional, for auth, unset by default + }) + err := tg.Send(context.Background(), "telegram:-1001480738202", "Hello, World!") + if err != nil { + log.Fatalf("problem sending message using telegram, %v", err) + } +} +``` + +#### HTML Formatting + +parseMode `HTML` supports [limited set of tags](https://core.telegram.org/bots/api#html-style), so `Telegram` provides `TelegramSupportedHTML` method which strips all unsupported tags and replaces `h1-h3` with `` and `h4-h6` with `` to preserve formatting. + +If you want to post text into HTML tag like text, you can use `EscapeTelegramText` method to escape it (by replacing symbols `&`, `<`, `>` with `&`, `<`, `>`). + +#### Authorisation + +You can use Telegram notifications as described above, just to send messages. But also, you can use `Telegram` to authorise users as a login method or to sign them up for notifications. Functions used for processing updates from users are `GetBotUsername`, `AddToken`, `CheckToken`, `Request`, and `Run` or `ProcessUpdate` (only one of two can be used at a time). + +Normal flow is following: +1. you run the `Run` goroutine +2. call `AddToken` and provide user with that token +3. user clicks on the link `https://t.me//?start=` +4. you call `CheckToken` to verify that the user clicked the link, and if so you will receive user's UserID + +Alternative flow is the same, but instead of running the `Run` goroutine, you set up process update flow separately (to use with [auth](https://github.com/go-pkgz/auth/blob/master/provider/telegram.go) as well, for example) and run `ProcessUpdate` when update is received. Example of such a setup can be seen [in Remark42](https://github.com/umputun/remark42/blob/c027dcd/backend/app/providers/telegram.go). + +### Slack + +`slack:` scheme akin to `mailto:` is supported. `title`, `titleLink`, `attachmentText` and query params are used: if they are defined, message would be sent with a [text attachment](https://api.slack.com/reference/messaging/attachments). Examples: + +- `slack:channel` +- `slack:channelID` +- `slack:userID` +- `slack:channel?title=title&attachmentText=test%20text&titleLink=https://example.org` + +```go +package main + +import ( + "context" + "log" + + "github.com/go-pkgz/notify" + "github.com/slack-go/slack" +) + +func main() { + wh := notify.NewSlack( + "token", + slack.OptionDebug(true), // optional, you can pass any slack.Options + ) + err := wh.Send(context.Background(), "slack:general", "Hello, World!") + if err != nil { + log.Fatalf("problem sending message using slack, %v", err) + } +} +``` + +### Webhook + +`http://` and `https://` schemas are supported. + +```go +package main + +import ( + "context" + "log" + "time" + + "github.com/go-pkgz/notify" +) + +func main() { + wh := notify.NewWebhook(notify.WebhookParams{ + Timeout: time.Second, // optional, default is 5 seconds + Headers: []string{"Content-Type:application/json,text/plain"}, // optional + }) + err := wh.Send(context.Background(), "https://example.org/webhook", "Hello, World!") + if err != nil { + log.Fatalf("problem sending message using webhook, %v", err) + } +} +``` + +## Status + +The library extracted from [remark42](https://github.com/umputun/remark) project. The original code in production use on multiple sites and seems to work fine. + +`go-pkgz/notify` library still in development and until version 1 released some breaking changes possible. diff --git a/backend/vendor/github.com/go-pkgz/notify/email.go b/backend/vendor/github.com/go-pkgz/notify/email.go new file mode 100644 index 00000000..d44c19c6 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/notify/email.go @@ -0,0 +1,132 @@ +package notify + +import ( + "context" + "fmt" + "net/mail" + "net/url" + "time" + + "github.com/go-pkgz/email" +) + +// SMTPParams contain settings for smtp server connection +type SMTPParams struct { + Host string // SMTP host + Port int // SMTP port + TLS bool // TLS auth + StartTLS bool // StartTLS auth + ContentType string // Content type + Charset string // Character set + Username string // username + Password string // password + TimeOut time.Duration // TCP connection timeout +} + +// Email notifications client +type Email struct { + SMTPParams + sender *email.Sender +} + +// NewEmail makes new Email object +func NewEmail(smtpParams SMTPParams) *Email { + var opts []email.Option + + if smtpParams.Username != "" { + opts = append(opts, email.Auth(smtpParams.Username, smtpParams.Password)) + } + + if smtpParams.ContentType != "" { + opts = append(opts, email.ContentType(smtpParams.ContentType)) + } + + if smtpParams.Charset != "" { + opts = append(opts, email.Charset(smtpParams.Charset)) + } + + if smtpParams.Port != 0 { + opts = append(opts, email.Port(smtpParams.Port)) + } + + if smtpParams.TimeOut != 0 { + opts = append(opts, email.TimeOut(smtpParams.TimeOut)) + } + + if smtpParams.TLS { + opts = append(opts, email.TLS(true)) + } + + if smtpParams.StartTLS { + opts = append(opts, email.STARTTLS(true)) + } + + sender := email.NewSender(smtpParams.Host, opts...) + + return &Email{sender: sender, SMTPParams: smtpParams} +} + +// Send sends the message over Email, with "from", "subject" and "unsubscribeLink" parsed from destination field +// with "mailto:" schema. +// "unsubscribeLink" passed as a header, https://support.google.com/mail/answer/81126 -> "Use one-click unsubscribe" +// +// Example: +// +// - mailto:"John Wayne"?subject=test-subj&from="Notifier" +// - mailto:addr1@example.org,addr2@example.org?subject=test-subj&from=notify@example.org&unsubscribeLink=http://example.org/unsubscribe +func (e *Email) Send(ctx context.Context, destination, text string) error { + emailParams, err := e.parseDestination(destination) + if err != nil { + return fmt.Errorf("problem parsing destination: %w", err) + } + + select { + case <-ctx.Done(): + return ctx.Err() + default: + return e.sender.Send(text, emailParams) + } +} + +// Schema returns schema prefix supported by this client +func (e *Email) Schema() string { + return "mailto" +} + +// String representation of Email object +func (e *Email) String() string { + str := fmt.Sprintf("email: with username '%s' at server %s:%d", e.Username, e.Host, e.Port) + if e.TLS { + str += " with TLS" + } + return str +} + +// parses "mailto:" URL and returns email parameters +func (e *Email) parseDestination(destination string) (email.Params, error) { + // parse URL + u, err := url.Parse(destination) + if err != nil { + return email.Params{}, err + } + if u.Scheme != "mailto" { + return email.Params{}, fmt.Errorf("unsupported scheme %s, should be mailto", u.Scheme) + } + + // parse destination address(es) + addresses, err := mail.ParseAddressList(u.Opaque) + if err != nil { + return email.Params{}, fmt.Errorf("problem parsing email recipients: %w", err) + } + destinations := []string{} + for _, addr := range addresses { + destinations = append(destinations, addr.String()) + } + + return email.Params{ + From: u.Query().Get("from"), + To: destinations, + Subject: u.Query().Get("subject"), + UnsubscribeLink: u.Query().Get("unsubscribeLink"), + }, nil +} diff --git a/backend/vendor/github.com/go-pkgz/notify/interface.go b/backend/vendor/github.com/go-pkgz/notify/interface.go new file mode 100644 index 00000000..b852e53d --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/notify/interface.go @@ -0,0 +1,28 @@ +// Package notify provides notification functionality. +package notify + +import ( + "context" + "fmt" + "strings" +) + +// Notifier defines common interface among all notifiers +type Notifier interface { + fmt.Stringer + Schema() string // returns schema prefix supported by this client + Send(ctx context.Context, destination, text string) error // sends message to provided destination +} + +// Send sends message to provided destination, picking the right one based on destination schema +func Send(ctx context.Context, notifiers []Notifier, destination, text string) error { + for _, n := range notifiers { + if strings.HasPrefix(destination, n.Schema()) { + return n.Send(ctx, destination, text) + } + } + if strings.Contains(destination, ":") { + return fmt.Errorf("unsupported destination schema: %s", strings.Split(destination, ":")[0]) + } + return fmt.Errorf("unsupported destination schema: %s", destination) +} diff --git a/backend/vendor/github.com/go-pkgz/notify/slack.go b/backend/vendor/github.com/go-pkgz/notify/slack.go new file mode 100644 index 00000000..52ee3b45 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/notify/slack.go @@ -0,0 +1,108 @@ +package notify + +import ( + "context" + "errors" + "fmt" + "net/url" + "strings" + + "github.com/slack-go/slack" +) + +// Slack notifications client +type Slack struct { + client *slack.Client +} + +// NewSlack makes Slack client for notifications +func NewSlack(token string, opts ...slack.Option) *Slack { + return &Slack{client: slack.New(token, opts...)} +} + +// Send sends the message over Slack, with "title", "titleLink" and "attachmentText" parsed from destination field +// with "slack:" schema same way "mailto:" schema is constructed. +// +// Example: +// +// - slack:channelName +// - slack:channelID +// - slack:userID +// - slack:channel?title=title&attachmentText=test%20text&titleLink=https://example.org +func (s *Slack) Send(ctx context.Context, destination, text string) error { + channelID, attachment, err := s.parseDestination(destination) + if err != nil { + return fmt.Errorf("problem parsing destination: %w", err) + } + options := []slack.MsgOption{slack.MsgOptionText(text, false)} + if attachment.Title != "" { + options = append(options, slack.MsgOptionAttachments(attachment)) + } + + select { + case <-ctx.Done(): + return ctx.Err() + default: + _, _, err = s.client.PostMessageContext(ctx, channelID, options...) + return err + } +} + +// Schema returns schema prefix supported by this client +func (s *Slack) Schema() string { + return "slack" +} + +func (s *Slack) String() string { + return "slack notifications destination" +} + +// parses "slack:" in a manner "mailto:" URL is parsed url and returns channelID and attachment. +// if channelID is channel name and not ID (starting with C for channel and with U for user), +// then it will be resolved to ID. +func (s *Slack) parseDestination(destination string) (string, slack.Attachment, error) { + // parse URL + u, err := url.Parse(destination) + if err != nil { + return "", slack.Attachment{}, err + } + if u.Scheme != "slack" { + return "", slack.Attachment{}, fmt.Errorf("unsupported scheme %s, should be slack", u.Scheme) + } + channelID := u.Opaque + if !strings.HasPrefix(u.Opaque, "C") && !strings.HasPrefix(u.Opaque, "U") { + channelID, err = s.findChannelIDByName(u.Opaque) + if err != nil { + return "", slack.Attachment{}, fmt.Errorf("problem retrieving channel ID for #%s: %w", u.Opaque, err) + } + } + + return channelID, + slack.Attachment{ + Title: u.Query().Get("title"), + TitleLink: u.Query().Get("titleLink"), + Text: u.Query().Get("attachmentText"), + }, nil +} + +func (s *Slack) findChannelIDByName(name string) (string, error) { + params := slack.GetConversationsParameters{} + for { + channels, next, err := s.client.GetConversations(¶ms) + if err != nil { + return "", err + } + + for _, channel := range channels { + if channel.Name == name { + return channel.ID, nil + } + } + + if next == "" { + break + } + params.Cursor = next + } + return "", errors.New("no such channel") +} diff --git a/backend/vendor/github.com/go-pkgz/notify/telegram.go b/backend/vendor/github.com/go-pkgz/notify/telegram.go new file mode 100644 index 00000000..ea7da783 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/notify/telegram.go @@ -0,0 +1,478 @@ +package notify + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + neturl "net/url" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + log "github.com/go-pkgz/lgr" + "github.com/go-pkgz/repeater" + "github.com/microcosm-cc/bluemonday" + "golang.org/x/net/html" +) + +// TelegramParams contain settings for telegram notifications +type TelegramParams struct { + Token string // token for telegram bot API interactions + Timeout time.Duration // http client timeout + ErrorMsg, SuccessMsg string // messages for successful and unsuccessful subscription requests to bot + + apiPrefix string // changed only in tests +} + +// Telegram notifications client +type Telegram struct { + TelegramParams + + // Identifier of the first update to be requested. + // Should be equal to LastSeenUpdateID + 1 + // See https://core.telegram.org/bots/api#getupdates + updateOffset int + apiPollInterval time.Duration // interval to check updates from Telegram API and answer to users + expiredCleanupInterval time.Duration // interval to check and clean up expired notification requests + username string // bot username + run int32 // non-zero if Run goroutine has started + requests struct { + sync.RWMutex + data map[string]tgAuthRequest + } +} + +// telegramMsg is used to send message trough Telegram bot API +type telegramMsg struct { + Text string `json:"text"` + ParseMode string `json:"parse_mode,omitempty"` +} + +type tgAuthRequest struct { + confirmed bool // whether login request has been confirmed and user info set + expires time.Time + telegramID string + user string + site string +} + +// TelegramBotInfo structure contains information about telegram bot, which is used from whole telegram API response +type TelegramBotInfo struct { + Username string `json:"username"` +} + +const telegramTimeOut = 5000 * time.Millisecond +const telegramAPIPrefix = "https://api.telegram.org/bot" +const tgPollInterval = time.Second * 5 +const tgCleanupInterval = time.Minute * 5 + +// NewTelegram makes telegram bot for notifications +func NewTelegram(params TelegramParams) (*Telegram, error) { + res := Telegram{TelegramParams: params} + + if res.apiPrefix == "" { + res.apiPrefix = telegramAPIPrefix + } + if res.Timeout == 0 { + res.Timeout = telegramTimeOut + } + + if res.SuccessMsg == "" { + res.SuccessMsg = "✅ You have successfully authenticated, check the web!" + } + + res.apiPollInterval = tgPollInterval + res.expiredCleanupInterval = tgCleanupInterval + log.Printf("[DEBUG] create new telegram notifier for api=%s, timeout=%s", res.apiPrefix, res.Timeout) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + botInfo, err := res.botInfo(ctx) + if err != nil { + return nil, fmt.Errorf("can't retrieve bot info from Telegram API: %w", err) + } + res.username = botInfo.Username + + res.requests.data = make(map[string]tgAuthRequest) + + return &res, nil +} + +// Send sends provided message to Telegram chat, with `parseMode` parsed from destination field (Markdown by default) +// with "telegram:" schema same way "mailto:" schema is constructed. +// +// Example: +// +// - telegram:channel +// - telegram:chatID // chatID is a number, like `-1001480738202` +// - telegram:channel?parseMode=HTML +func (t *Telegram) Send(ctx context.Context, destination, text string) error { + chatID, parseMode, err := t.parseDestination(destination) + if err != nil { + return fmt.Errorf("problem parsing destination: %w", err) + } + + body := telegramMsg{Text: text, ParseMode: parseMode} + b, err := json.Marshal(body) + if err != nil { + return err + } + + url := fmt.Sprintf("sendMessage?chat_id=%s&disable_web_page_preview=true", chatID) + return t.Request(ctx, url, b, &struct{}{}) +} + +// TelegramSupportedHTML returns HTML with only tags allowed in Telegram HTML message payload, also trims ending newlines +// +// https://core.telegram.org/bots/api#html-style +func TelegramSupportedHTML(htmlText string) string { + adjustedHTMLText := adjustHTMLTags(htmlText) + p := bluemonday.NewPolicy() + p.AllowElements("b", "strong", "i", "em", "u", "ins", "s", "strike", "del", "a", "code", "pre") + p.AllowAttrs("href").OnElements("a") + p.AllowAttrs("class").OnElements("code") + return strings.TrimRight(p.Sanitize(adjustedHTMLText), "\n") +} + +// EscapeTelegramText returns text sanitized of symbols not allowed inside other HTML tags in Telegram HTML message payload +// +// https://core.telegram.org/bots/api#html-style +func EscapeTelegramText(text string) string { + // order is important + text = strings.ReplaceAll(text, "&", "&") + text = strings.ReplaceAll(text, "<", "<") + text = strings.ReplaceAll(text, ">", ">") + return text +} + +// telegram not allow h1-h6 tags +// replace these tags with a combination of and for visual distinction +func adjustHTMLTags(htmlText string) string { + buff := strings.Builder{} + tokenizer := html.NewTokenizer(strings.NewReader(htmlText)) + for { + if tokenizer.Next() == html.ErrorToken { + return buff.String() + } + token := tokenizer.Token() + switch token.Type { + case html.StartTagToken, html.EndTagToken: + switch token.Data { + case "h1", "h2", "h3": + if token.Type == html.StartTagToken { + buff.WriteString("") + } + if token.Type == html.EndTagToken { + buff.WriteString("") + } + case "h4", "h5", "h6": + if token.Type == html.StartTagToken { + buff.WriteString("") + } + if token.Type == html.EndTagToken { + buff.WriteString("") + } + default: + buff.WriteString(token.String()) + } + default: + buff.WriteString(token.String()) + } + } +} + +// TelegramUpdate contains update information, which is used from whole telegram API response +type TelegramUpdate struct { + Result []struct { + UpdateID int `json:"update_id"` + Message struct { + Chat struct { + ID int `json:"id"` + Name string `json:"first_name"` + Type string `json:"type"` + } `json:"chat"` + Text string `json:"text"` + } `json:"message"` + } `json:"result"` +} + +// GetBotUsername returns bot username +func (t *Telegram) GetBotUsername() string { + return t.username +} + +// AddToken adds token +func (t *Telegram) AddToken(token, user, site string, expires time.Time) { + t.requests.Lock() + t.requests.data[token] = tgAuthRequest{ + expires: expires, + user: user, + site: site, + } + t.requests.Unlock() +} + +// CheckToken verifies incoming token, returns the user address if it's confirmed and empty string otherwise +func (t *Telegram) CheckToken(token, user string) (telegram, site string, err error) { + t.requests.RLock() + authRequest, ok := t.requests.data[token] + t.requests.RUnlock() + + if !ok { + return "", "", errors.New("request is not found") + } + + if time.Now().After(authRequest.expires) { + t.requests.Lock() + delete(t.requests.data, token) + t.requests.Unlock() + return "", "", errors.New("request expired") + } + + if !authRequest.confirmed { + return "", "", errors.New("request is not verified yet") + } + + if authRequest.user != user { + return "", "", errors.New("user does not match original requester") + } + + // Delete request + t.requests.Lock() + delete(t.requests.data, token) + t.requests.Unlock() + + return authRequest.telegramID, authRequest.site, nil +} + +// Run starts processing login requests sent in Telegram, required for user notifications to work +// Blocks caller +func (t *Telegram) Run(ctx context.Context) { + atomic.AddInt32(&t.run, 1) + processUpdatedTicker := time.NewTicker(t.apiPollInterval) + cleanupTicker := time.NewTicker(t.expiredCleanupInterval) + + for { + select { + case <-ctx.Done(): + processUpdatedTicker.Stop() + cleanupTicker.Stop() + atomic.AddInt32(&t.run, -1) + return + case <-processUpdatedTicker.C: + updates, err := t.getUpdates(ctx) + if err != nil { + log.Printf("[WARN] Error while getting telegram updates: %v", err) + continue + } + t.processUpdates(ctx, updates) + case <-cleanupTicker.C: + now := time.Now() + t.requests.Lock() + for key, req := range t.requests.data { + if now.After(req.expires) { + delete(t.requests.data, key) + } + } + t.requests.Unlock() + } + } +} + +// ProcessUpdate is alternative to Run, it processes provided plain text update from Telegram +// so that caller could get updates and send it not only there but to multiple sources +func (t *Telegram) ProcessUpdate(ctx context.Context, textUpdate string) error { + if atomic.LoadInt32(&t.run) != 0 { + return errors.New("the Run goroutine should not be used with ProcessUpdate") + } + defer func() { + // as Run goroutine is not running, clean up old requests on each update + // even if we hit json decode error + now := time.Now() + t.requests.Lock() + for key, req := range t.requests.data { + if now.After(req.expires) { + delete(t.requests.data, key) + } + } + t.requests.Unlock() + }() + var updates TelegramUpdate + if err := json.Unmarshal([]byte(textUpdate), &updates); err != nil { + return fmt.Errorf("failed to decode provided telegram update: %w", err) + } + t.processUpdates(ctx, &updates) + return nil +} + +// Schema returns schema prefix supported by this client +func (t *Telegram) Schema() string { + return "telegram" +} + +func (t *Telegram) String() string { + return "telegram notifications destination" +} + +// parses "telegram:" in a manner "mailto:" URL is parsed url and returns chatID and parseMode. +// if chatID is channel name and not a numerical ID, `@` will be added to it +func (t *Telegram) parseDestination(destination string) (chatID, parseMode string, err error) { + // parse URL + u, err := neturl.Parse(destination) + if err != nil { + return "", "", err + } + if u.Scheme != "telegram" { + return "", "", fmt.Errorf("unsupported scheme %s, should be telegram", u.Scheme) + } + + chatID = u.Opaque + if _, err := strconv.ParseInt(chatID, 10, 64); err != nil { + chatID = "@" + chatID // if chatID not a number enforce @ prefix + } + + parseMode = "Markdown" + if u.Query().Get("parseMode") != "" { + parseMode = u.Query().Get("parseMode") + } + + return chatID, parseMode, nil +} + +// getUpdates fetches incoming updates +func (t *Telegram) getUpdates(ctx context.Context) (*TelegramUpdate, error) { + url := `getUpdates?allowed_updates=["message"]` + if t.updateOffset != 0 { + url += fmt.Sprintf("&offset=%d", t.updateOffset) + } + + var result TelegramUpdate + + err := t.Request(ctx, url, nil, &result) + if err != nil { + return nil, fmt.Errorf("failed to fetch updates: %w", err) + } + + for _, u := range result.Result { + if u.UpdateID >= t.updateOffset { + t.updateOffset = u.UpdateID + 1 + } + } + + return &result, nil +} + +// processUpdates processes a batch of updates from telegram servers +func (t *Telegram) processUpdates(ctx context.Context, updates *TelegramUpdate) { + for _, update := range updates.Result { + if update.Message.Chat.Type != "private" { + continue + } + + if !strings.HasPrefix(update.Message.Text, "/start ") { + continue + } + + token := strings.TrimPrefix(update.Message.Text, "/start ") + + t.requests.RLock() + authRequest, ok := t.requests.data[token] + if !ok { // No such token + t.requests.RUnlock() + if t.ErrorMsg != "" { + if err := t.sendText(ctx, update.Message.Chat.ID, t.ErrorMsg); err != nil { + log.Printf("[WARN] failed to notify telegram peer: %v", err) + } + } + continue + } + t.requests.RUnlock() + + authRequest.confirmed = true + authRequest.telegramID = strconv.Itoa(update.Message.Chat.ID) + + t.requests.Lock() + t.requests.data[token] = authRequest + t.requests.Unlock() + + if err := t.sendText(ctx, update.Message.Chat.ID, t.SuccessMsg); err != nil { + log.Printf("[ERROR] failed to notify telegram peer: %v", err) + } + } +} + +// sendText sends a plain text message to telegram peer +func (t *Telegram) sendText(ctx context.Context, recipientID int, msg string) error { + url := fmt.Sprintf("sendMessage?chat_id=%d&text=%s", recipientID, neturl.PathEscape(msg)) + return t.Request(ctx, url, nil, &struct{}{}) +} + +// botInfo returns info about configured bot +func (t *Telegram) botInfo(ctx context.Context) (*TelegramBotInfo, error) { + var resp = struct { + Result *TelegramBotInfo `json:"result"` + }{} + + err := t.Request(ctx, "getMe", nil, &resp) + if err != nil { + return nil, err + } + if resp.Result == nil { + return nil, errors.New("received empty result") + } + + return resp.Result, nil +} + +// Request makes a request to the Telegram API and return the result +func (t *Telegram) Request(ctx context.Context, method string, b []byte, data interface{}) error { + return repeater.NewDefault(3, time.Millisecond*250).Do(ctx, func() error { + url := fmt.Sprintf("%s%s/%s", t.apiPrefix, t.Token, method) + + var req *http.Request + var err error + if b == nil { + req, err = http.NewRequestWithContext(ctx, "GET", url, http.NoBody) + } else { + req, err = http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + } + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + client := http.Client{Timeout: t.Timeout} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return t.parseError(resp.Body, resp.StatusCode) + } + + if err = json.NewDecoder(resp.Body).Decode(data); err != nil { + return fmt.Errorf("failed to decode json response: %w", err) + } + + return nil + }) +} + +func (t *Telegram) parseError(r io.Reader, statusCode int) error { + tgErr := struct { + Description string `json:"description"` + }{} + if err := json.NewDecoder(r).Decode(&tgErr); err != nil { + return fmt.Errorf("unexpected telegram API status code %d", statusCode) + } + return fmt.Errorf("unexpected telegram API status code %d, error: %q", statusCode, tgErr.Description) +} diff --git a/backend/vendor/github.com/go-pkgz/notify/webhook.go b/backend/vendor/github.com/go-pkgz/notify/webhook.go new file mode 100644 index 00000000..6cdffeed --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/notify/webhook.go @@ -0,0 +1,96 @@ +package notify + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +const webhookTimeOut = 5000 * time.Millisecond + +// WebhookParams contain settings for webhook notifications +type WebhookParams struct { + Timeout time.Duration + Headers []string // headers in format "header:value" +} + +// Webhook notifications client +type Webhook struct { + WebhookParams + webhookClient webhookClient +} + +// webhookClient defines an interface of client for webhook +type webhookClient interface { + Do(*http.Request) (*http.Response, error) +} + +// NewWebhook makes Webhook +func NewWebhook(params WebhookParams) *Webhook { + res := &Webhook{WebhookParams: params} + + if res.Timeout == 0 { + res.Timeout = webhookTimeOut + } + + res.webhookClient = &http.Client{Timeout: res.Timeout} + + return res +} + +// Send sends Webhook notification. Destination field is expected to have http:// or https:// schema. +// +// Example: +// +// - https://example.com/webhook +func (wh *Webhook) Send(ctx context.Context, destination, text string) error { + payload := bytes.NewBufferString(text) + httpReq, err := http.NewRequestWithContext(ctx, "POST", destination, payload) + if err != nil { + return fmt.Errorf("unable to create webhook request: %w", err) + } + + for _, h := range wh.Headers { + elems := strings.Split(h, ":") + if len(elems) != 2 { + continue + } + httpReq.Header.Set(strings.TrimSpace(elems[0]), strings.TrimSpace(elems[1])) + } + + resp, err := wh.webhookClient.Do(httpReq) + if err != nil { + return fmt.Errorf("webhook request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + errMsg := fmt.Sprintf("webhook request failed with non-OK status code: %d", resp.StatusCode) + respBody, e := io.ReadAll(resp.Body) + if e != nil { + return errors.New(errMsg) + } + return fmt.Errorf("%s, body: %s", errMsg, respBody) + } + + return nil +} + +// Schema returns schema prefix supported by this client +func (wh *Webhook) Schema() string { + return "http" +} + +// String describes the webhook instance +func (wh *Webhook) String() string { + str := fmt.Sprintf("webhook notification with timeout %s", wh.Timeout) + if wh.Headers != nil { + str += fmt.Sprintf(" and headers %v", wh.Headers) + } + return str +} diff --git a/backend/vendor/github.com/slack-go/slack/apps.go b/backend/vendor/github.com/slack-go/slack/apps.go index 4f60da0a..10d42975 100644 --- a/backend/vendor/github.com/slack-go/slack/apps.go +++ b/backend/vendor/github.com/slack-go/slack/apps.go @@ -44,6 +44,10 @@ func (api *Client) ListEventAuthorizationsContext(ctx context.Context, eventCont } func (api *Client) UninstallApp(clientID, clientSecret string) error { + return api.UninstallAppContext(context.Background(), clientID, clientSecret) +} + +func (api *Client) UninstallAppContext(ctx context.Context, clientID, clientSecret string) error { values := url.Values{ "client_id": {clientID}, "client_secret": {clientSecret}, @@ -51,7 +55,7 @@ func (api *Client) UninstallApp(clientID, clientSecret string) error { response := SlackResponse{} - err := api.getMethod(context.Background(), "apps.uninstall", api.token, values, &response) + err := api.getMethod(ctx, "apps.uninstall", api.token, values, &response) if err != nil { return err } diff --git a/backend/vendor/github.com/slack-go/slack/audit.go b/backend/vendor/github.com/slack-go/slack/audit.go index 041ffd77..a3ea7ebd 100644 --- a/backend/vendor/github.com/slack-go/slack/audit.go +++ b/backend/vendor/github.com/slack-go/slack/audit.go @@ -39,6 +39,16 @@ type AuditEntry struct { UA string `json:"ua"` IPAddress string `json:"ip_address"` } `json:"context"` + Details struct { + NewValue interface{} `json:"new_value"` + PreviousValue interface{} `json:"previous_value"` + MobileOnly bool `json:"mobile_only"` + WebOnly bool `json:"web_only"` + NonSSOOnly bool `json:"non_sso_only"` + ExportType string `json:"export_type"` + ExportStart string `json:"export_start_ts"` + ExportEnd string `json:"export_end_ts"` + } `json:"details"` } type AuditUser struct { diff --git a/backend/vendor/github.com/slack-go/slack/block_element.go b/backend/vendor/github.com/slack-go/slack/block_element.go index 21abb018..643529ff 100644 --- a/backend/vendor/github.com/slack-go/slack/block_element.go +++ b/backend/vendor/github.com/slack-go/slack/block_element.go @@ -167,6 +167,12 @@ func (s *ButtonBlockElement) WithStyle(style Style) *ButtonBlockElement { return s } +// WithConfirm adds a confirmation dialogue to the button object and returns the modified ButtonBlockElement +func (s *ButtonBlockElement) WithConfirm(confirm *ConfirmationBlockObject) *ButtonBlockElement { + s.Confirm = confirm + return s +} + // NewButtonBlockElement returns an instance of a new button element to be used within a block func NewButtonBlockElement(actionID, value string, text *TextBlockObject) *ButtonBlockElement { return &ButtonBlockElement{ diff --git a/backend/vendor/github.com/slack-go/slack/block_object.go b/backend/vendor/github.com/slack-go/slack/block_object.go index 5ced7f92..f70405eb 100644 --- a/backend/vendor/github.com/slack-go/slack/block_object.go +++ b/backend/vendor/github.com/slack-go/slack/block_object.go @@ -187,8 +187,9 @@ func (s ConfirmationBlockObject) validateType() MessageObjectType { } // WithStyle add styling to confirmation object -func (s *ConfirmationBlockObject) WithStyle(style Style) { +func (s *ConfirmationBlockObject) WithStyle(style Style) *ConfirmationBlockObject { s.Style = style + return s } // NewConfirmationBlockObject returns an instance of a new Confirmation Block Object diff --git a/backend/vendor/github.com/slack-go/slack/chat.go b/backend/vendor/github.com/slack-go/slack/chat.go index 493b65b6..34848d15 100644 --- a/backend/vendor/github.com/slack-go/slack/chat.go +++ b/backend/vendor/github.com/slack-go/slack/chat.go @@ -86,12 +86,7 @@ func NewPostMessageParameters() PostMessageParameters { // DeleteMessage deletes a message in a channel func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) { - respChannel, respTimestamp, _, err := api.SendMessageContext( - context.Background(), - channel, - MsgOptionDelete(messageTimestamp), - ) - return respChannel, respTimestamp, err + return api.DeleteMessageContext(context.Background(), channel, messageTimestamp) } // DeleteMessageContext deletes a message in a channel with a custom context @@ -108,8 +103,15 @@ func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTim // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. func (api *Client) ScheduleMessage(channelID, postAt string, options ...MsgOption) (string, string, error) { + return api.ScheduleMessageContext(context.Background(), channelID, postAt, options...) +} + +// ScheduleMessageContext sends a message to a channel with a custom context +// +// For more details, see ScheduleMessage documentation. +func (api *Client) ScheduleMessageContext(ctx context.Context, channelID, postAt string, options ...MsgOption) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessageContext( - context.Background(), + ctx, channelID, MsgOptionSchedule(postAt), MsgOptionCompose(options...), @@ -121,13 +123,7 @@ func (api *Client) ScheduleMessage(channelID, postAt string, options ...MsgOptio // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. func (api *Client) PostMessage(channelID string, options ...MsgOption) (string, string, error) { - respChannel, respTimestamp, _, err := api.SendMessageContext( - context.Background(), - channelID, - MsgOptionPost(), - MsgOptionCompose(options...), - ) - return respChannel, respTimestamp, err + return api.PostMessageContext(context.Background(), channelID, options...) } // PostMessageContext sends a message to a channel with a custom context @@ -146,12 +142,7 @@ func (api *Client) PostMessageContext(ctx context.Context, channelID string, opt // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption) (string, error) { - return api.PostEphemeralContext( - context.Background(), - channelID, - userID, - options..., - ) + return api.PostEphemeralContext(context.Background(), channelID, userID, options...) } // PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context @@ -168,12 +159,7 @@ func (api *Client) PostEphemeralContext(ctx context.Context, channelID, userID s // UpdateMessage updates a message in a channel func (api *Client) UpdateMessage(channelID, timestamp string, options ...MsgOption) (string, string, string, error) { - return api.SendMessageContext( - context.Background(), - channelID, - MsgOptionUpdate(timestamp), - MsgOptionCompose(options...), - ) + return api.UpdateMessageContext(context.Background(), channelID, timestamp, options...) } // UpdateMessageContext updates a message in a channel @@ -225,7 +211,7 @@ func (api *Client) SendMessageContext(ctx context.Context, channelID string, opt response chatResponseFull ) - if req, parser, err = buildSender(api.endpoint, options...).BuildRequest(api.token, channelID); err != nil { + if req, parser, err = buildSender(api.endpoint, options...).BuildRequestContext(ctx, api.token, channelID); err != nil { return "", "", "", err } @@ -306,6 +292,10 @@ type sendConfig struct { } func (t sendConfig) BuildRequest(token, channelID string) (req *http.Request, _ func(*chatResponseFull) responseParser, err error) { + return t.BuildRequestContext(context.Background(), token, channelID) +} + +func (t sendConfig) BuildRequestContext(ctx context.Context, token, channelID string) (req *http.Request, _ func(*chatResponseFull) responseParser, err error) { if t, err = applyMsgOptions(token, channelID, t.apiurl, t.options...); err != nil { return nil, nil, err } @@ -320,9 +310,9 @@ func (t sendConfig) BuildRequest(token, channelID string) (req *http.Request, _ responseType: t.responseType, replaceOriginal: t.replaceOriginal, deleteOriginal: t.deleteOriginal, - }.BuildRequest() + }.BuildRequestContext(ctx) default: - return formSender{endpoint: t.endpoint, values: t.values}.BuildRequest() + return formSender{endpoint: t.endpoint, values: t.values}.BuildRequestContext(ctx) } } @@ -332,7 +322,11 @@ type formSender struct { } func (t formSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { - req, err := formReq(t.endpoint, t.values) + return t.BuildRequestContext(context.Background()) +} + +func (t formSender) BuildRequestContext(ctx context.Context) (*http.Request, func(*chatResponseFull) responseParser, error) { + req, err := formReq(ctx, t.endpoint, t.values) return req, func(resp *chatResponseFull) responseParser { return newJSONParser(resp) }, err @@ -349,7 +343,11 @@ type responseURLSender struct { } func (t responseURLSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { - req, err := jsonReq(t.endpoint, Msg{ + return t.BuildRequestContext(context.Background()) +} + +func (t responseURLSender) BuildRequestContext(ctx context.Context) (*http.Request, func(*chatResponseFull) responseParser, error) { + req, err := jsonReq(ctx, t.endpoint, Msg{ Text: t.values.Get("text"), Timestamp: t.values.Get("ts"), Attachments: t.attachments, diff --git a/backend/vendor/github.com/slack-go/slack/files.go b/backend/vendor/github.com/slack-go/slack/files.go index 00c255bc..e7e71c49 100644 --- a/backend/vendor/github.com/slack-go/slack/files.go +++ b/backend/vendor/github.com/slack-go/slack/files.go @@ -202,7 +202,14 @@ func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, // GetFile retreives a given file from its private download URL func (api *Client) GetFile(downloadURL string, writer io.Writer) error { - return downloadFile(api.httpclient, api.token, downloadURL, writer, api) + return api.GetFileContext(context.Background(), downloadURL, writer) +} + +// GetFileContext retreives a given file from its private download URL with a custom context +// +// For more details, see GetFile documentation. +func (api *Client) GetFileContext(ctx context.Context, downloadURL string, writer io.Writer) error { + return downloadFile(ctx, api.httpclient, api.token, downloadURL, writer, api) } // GetFiles retrieves all files according to the parameters given @@ -210,40 +217,6 @@ func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) return api.GetFilesContext(context.Background(), params) } -// ListFiles retrieves all files according to the parameters given. Uses cursor based pagination. -func (api *Client) ListFiles(params ListFilesParameters) ([]File, *ListFilesParameters, error) { - return api.ListFilesContext(context.Background(), params) -} - -// ListFilesContext retrieves all files according to the parameters given with a custom context. Uses cursor based pagination. -func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParameters) ([]File, *ListFilesParameters, error) { - values := url.Values{ - "token": {api.token}, - } - - if params.User != DEFAULT_FILES_USER { - values.Add("user", params.User) - } - if params.Channel != DEFAULT_FILES_CHANNEL { - values.Add("channel", params.Channel) - } - if params.Limit != DEFAULT_FILES_COUNT { - values.Add("limit", strconv.Itoa(params.Limit)) - } - if params.Cursor != "" { - values.Add("cursor", params.Cursor) - } - - response, err := api.fileRequest(ctx, "files.list", values) - if err != nil { - return nil, nil, err - } - - params.Cursor = response.Metadata.Cursor - - return response.Files, ¶ms, nil -} - // GetFilesContext retrieves all files according to the parameters given with a custom context func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameters) ([]File, *Paging, error) { values := url.Values{ @@ -281,6 +254,42 @@ func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameter return response.Files, &response.Paging, nil } +// ListFiles retrieves all files according to the parameters given. Uses cursor based pagination. +func (api *Client) ListFiles(params ListFilesParameters) ([]File, *ListFilesParameters, error) { + return api.ListFilesContext(context.Background(), params) +} + +// ListFilesContext retrieves all files according to the parameters given with a custom context. +// +// For more details, see ListFiles documentation. +func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParameters) ([]File, *ListFilesParameters, error) { + values := url.Values{ + "token": {api.token}, + } + + if params.User != DEFAULT_FILES_USER { + values.Add("user", params.User) + } + if params.Channel != DEFAULT_FILES_CHANNEL { + values.Add("channel", params.Channel) + } + if params.Limit != DEFAULT_FILES_COUNT { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + + response, err := api.fileRequest(ctx, "files.list", values) + if err != nil { + return nil, nil, err + } + + params.Cursor = response.Metadata.Cursor + + return response.Files, ¶ms, nil +} + // UploadFile uploads a file func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) { return api.UploadFileContext(context.Background(), params) diff --git a/backend/vendor/github.com/slack-go/slack/info.go b/backend/vendor/github.com/slack-go/slack/info.go index 16fa667b..fde2bc98 100644 --- a/backend/vendor/github.com/slack-go/slack/info.go +++ b/backend/vendor/github.com/slack-go/slack/info.go @@ -321,9 +321,13 @@ type UserPrefs struct { } func (api *Client) GetUserPrefs() (*UserPrefsCarrier, error) { + return api.GetUserPrefsContext(context.Background()) +} + +func (api *Client) GetUserPrefsContext(ctx context.Context) (*UserPrefsCarrier, error) { response := UserPrefsCarrier{} - err := api.getMethod(context.Background(), "users.prefs.get", api.token, url.Values{}, &response) + err := api.getMethod(ctx, "users.prefs.get", api.token, url.Values{}, &response) if err != nil { return nil, err } diff --git a/backend/vendor/github.com/slack-go/slack/interactions.go b/backend/vendor/github.com/slack-go/slack/interactions.go index 9a519e27..e362caa8 100644 --- a/backend/vendor/github.com/slack-go/slack/interactions.go +++ b/backend/vendor/github.com/slack-go/slack/interactions.go @@ -28,32 +28,34 @@ const ( InteractionTypeViewSubmission = InteractionType("view_submission") InteractionTypeViewClosed = InteractionType("view_closed") InteractionTypeShortcut = InteractionType("shortcut") + InteractionTypeWorkflowStepEdit = InteractionType("workflow_step_edit") ) // InteractionCallback is sent from slack when a user interactions with a button or dialog. type InteractionCallback struct { - Type InteractionType `json:"type"` - Token string `json:"token"` - CallbackID string `json:"callback_id"` - ResponseURL string `json:"response_url"` - TriggerID string `json:"trigger_id"` - ActionTs string `json:"action_ts"` - Team Team `json:"team"` - Channel Channel `json:"channel"` - User User `json:"user"` - OriginalMessage Message `json:"original_message"` - Message Message `json:"message"` - Name string `json:"name"` - Value string `json:"value"` - MessageTs string `json:"message_ts"` - AttachmentID string `json:"attachment_id"` - ActionCallback ActionCallbacks `json:"actions"` - View View `json:"view"` - ActionID string `json:"action_id"` - APIAppID string `json:"api_app_id"` - BlockID string `json:"block_id"` - Container Container `json:"container"` - Enterprise Enterprise `json:"enterprise"` + Type InteractionType `json:"type"` + Token string `json:"token"` + CallbackID string `json:"callback_id"` + ResponseURL string `json:"response_url"` + TriggerID string `json:"trigger_id"` + ActionTs string `json:"action_ts"` + Team Team `json:"team"` + Channel Channel `json:"channel"` + User User `json:"user"` + OriginalMessage Message `json:"original_message"` + Message Message `json:"message"` + Name string `json:"name"` + Value string `json:"value"` + MessageTs string `json:"message_ts"` + AttachmentID string `json:"attachment_id"` + ActionCallback ActionCallbacks `json:"actions"` + View View `json:"view"` + ActionID string `json:"action_id"` + APIAppID string `json:"api_app_id"` + BlockID string `json:"block_id"` + Container Container `json:"container"` + Enterprise Enterprise `json:"enterprise"` + WorkflowStep InteractionWorkflowStep `json:"workflow_step"` DialogSubmissionCallback ViewSubmissionCallback ViewClosedCallback @@ -134,6 +136,14 @@ type Enterprise struct { Name string `json:"name"` } +type InteractionWorkflowStep struct { + WorkflowStepEditID string `json:"workflow_step_edit_id,omitempty"` + WorkflowID string `json:"workflow_id"` + StepID string `json:"step_id"` + Inputs *WorkflowStepInputs `json:"inputs,omitempty"` + Outputs *[]WorkflowStepOutput `json:"outputs,omitempty"` +} + // ActionCallback is a convenience struct defined to allow dynamic unmarshalling of // the "actions" value in Slack's JSON response, which varies depending on block type type ActionCallbacks struct { diff --git a/backend/vendor/github.com/slack-go/slack/messageID.go b/backend/vendor/github.com/slack-go/slack/messageID.go index a17472b4..689ee80d 100644 --- a/backend/vendor/github.com/slack-go/slack/messageID.go +++ b/backend/vendor/github.com/slack-go/slack/messageID.go @@ -1,6 +1,6 @@ package slack -import "sync" +import "sync/atomic" // IDGenerator provides an interface for generating integer ID values. type IDGenerator interface { @@ -11,20 +11,20 @@ type IDGenerator interface { // concurrent use by multiple goroutines. func NewSafeID(startID int) IDGenerator { return &safeID{ - nextID: startID, - mutex: &sync.Mutex{}, + nextID: int64(startID), } } type safeID struct { - nextID int - mutex *sync.Mutex + nextID int64 } +// make sure safeID implements the IDGenerator interface. +var _ IDGenerator = (*safeID)(nil) + +// Next implements IDGenerator.Next. func (s *safeID) Next() int { - s.mutex.Lock() - defer s.mutex.Unlock() - id := s.nextID - s.nextID++ - return id + id := atomic.AddInt64(&s.nextID, 1) + + return int(id) } diff --git a/backend/vendor/github.com/slack-go/slack/misc.go b/backend/vendor/github.com/slack-go/slack/misc.go index 5272e7c4..804724d7 100644 --- a/backend/vendor/github.com/slack-go/slack/misc.go +++ b/backend/vendor/github.com/slack-go/slack/misc.go @@ -66,29 +66,27 @@ func (e *RateLimitedError) Retryable() bool { } func fileUploadReq(ctx context.Context, path string, values url.Values, r io.Reader) (*http.Request, error) { - req, err := http.NewRequest("POST", path, r) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, path, r) if err != nil { return nil, err } - req = req.WithContext(ctx) - req.URL.RawQuery = (values).Encode() + req.URL.RawQuery = values.Encode() return req, nil } -func downloadFile(client httpClient, token string, downloadURL string, writer io.Writer, d Debug) error { +func downloadFile(ctx context.Context, client httpClient, token string, downloadURL string, writer io.Writer, d Debug) error { if downloadURL == "" { return fmt.Errorf("received empty download URL") } - req, err := http.NewRequest("GET", downloadURL, &bytes.Buffer{}) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, &bytes.Buffer{}) if err != nil { return err } var bearer = "Bearer " + token req.Header.Add("Authorization", bearer) - req.WithContext(context.Background()) resp, err := client.Do(req) if err != nil { @@ -107,8 +105,8 @@ func downloadFile(client httpClient, token string, downloadURL string, writer io return err } -func formReq(endpoint string, values url.Values) (req *http.Request, err error) { - if req, err = http.NewRequest("POST", endpoint, strings.NewReader(values.Encode())); err != nil { +func formReq(ctx context.Context, endpoint string, values url.Values) (req *http.Request, err error) { + if req, err = http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(values.Encode())); err != nil { return nil, err } @@ -116,13 +114,13 @@ func formReq(endpoint string, values url.Values) (req *http.Request, err error) return req, nil } -func jsonReq(endpoint string, body interface{}) (req *http.Request, err error) { +func jsonReq(ctx context.Context, endpoint string, body interface{}) (req *http.Request, err error) { buffer := bytes.NewBuffer([]byte{}) if err = json.NewEncoder(buffer).Encode(body); err != nil { return nil, err } - if req, err = http.NewRequest("POST", endpoint, buffer); err != nil { + if req, err = http.NewRequestWithContext(ctx, http.MethodPost, endpoint, buffer); err != nil { return nil, err } @@ -184,7 +182,6 @@ func postWithMultipartResponse(ctx context.Context, client httpClient, path, nam } req.Header.Add("Content-Type", wr.FormDataContentType()) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - req = req.WithContext(ctx) resp, err := client.Do(req) if err != nil { @@ -206,7 +203,6 @@ func postWithMultipartResponse(ctx context.Context, client httpClient, path, nam } func doPost(ctx context.Context, client httpClient, req *http.Request, parser responseParser, d Debug) error { - req = req.WithContext(ctx) resp, err := client.Do(req) if err != nil { return err @@ -224,7 +220,7 @@ func doPost(ctx context.Context, client httpClient, req *http.Request, parser re // post JSON. func postJSON(ctx context.Context, client httpClient, endpoint, token string, json []byte, intf interface{}, d Debug) error { reqBody := bytes.NewBuffer(json) - req, err := http.NewRequest("POST", endpoint, reqBody) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, reqBody) if err != nil { return err } @@ -237,7 +233,7 @@ func postJSON(ctx context.Context, client httpClient, endpoint, token string, js // post a url encoded form. func postForm(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d Debug) error { reqBody := strings.NewReader(values.Encode()) - req, err := http.NewRequest("POST", endpoint, reqBody) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, reqBody) if err != nil { return err } @@ -246,7 +242,7 @@ func postForm(ctx context.Context, client httpClient, endpoint string, values ur } func getResource(ctx context.Context, client httpClient, endpoint, token string, values url.Values, intf interface{}, d Debug) error { - req, err := http.NewRequest("GET", endpoint, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return err } diff --git a/backend/vendor/github.com/slack-go/slack/reminders.go b/backend/vendor/github.com/slack-go/slack/reminders.go index ae1da866..53d67c03 100644 --- a/backend/vendor/github.com/slack-go/slack/reminders.go +++ b/backend/vendor/github.com/slack-go/slack/reminders.go @@ -52,10 +52,17 @@ func (api *Client) doReminders(ctx context.Context, path string, values url.Valu // // See https://api.slack.com/methods/reminders.list func (api *Client) ListReminders() ([]*Reminder, error) { + return api.ListRemindersContext(context.Background()) +} + +// ListRemindersContext lists all the reminders created by or for the authenticated user with a custom context +// +// For more details, see ListReminders documentation. +func (api *Client) ListRemindersContext(ctx context.Context) ([]*Reminder, error) { values := url.Values{ "token": {api.token}, } - return api.doReminders(context.Background(), "reminders.list", values) + return api.doReminders(ctx, "reminders.list", values) } // AddChannelReminder adds a reminder for a channel. @@ -64,13 +71,20 @@ func (api *Client) ListReminders() ([]*Reminder, error) { // reminders on a channel is currently undocumented but has been tested to // work) func (api *Client) AddChannelReminder(channelID, text, time string) (*Reminder, error) { + return api.AddChannelReminderContext(context.Background(), channelID, text, time) +} + +// AddChannelReminderContext adds a reminder for a channel with a custom context +// +// For more details, see AddChannelReminder documentation. +func (api *Client) AddChannelReminderContext(ctx context.Context, channelID, text, time string) (*Reminder, error) { values := url.Values{ "token": {api.token}, "text": {text}, "time": {time}, "channel": {channelID}, } - return api.doReminder(context.Background(), "reminders.add", values) + return api.doReminder(ctx, "reminders.add", values) } // AddUserReminder adds a reminder for a user. @@ -79,25 +93,39 @@ func (api *Client) AddChannelReminder(channelID, text, time string) (*Reminder, // reminders on a channel is currently undocumented but has been tested to // work) func (api *Client) AddUserReminder(userID, text, time string) (*Reminder, error) { + return api.AddUserReminderContext(context.Background(), userID, text, time) +} + +// AddUserReminderContext adds a reminder for a user with a custom context +// +// For more details, see AddUserReminder documentation. +func (api *Client) AddUserReminderContext(ctx context.Context, userID, text, time string) (*Reminder, error) { values := url.Values{ "token": {api.token}, "text": {text}, "time": {time}, "user": {userID}, } - return api.doReminder(context.Background(), "reminders.add", values) + return api.doReminder(ctx, "reminders.add", values) } // DeleteReminder deletes an existing reminder. // // See https://api.slack.com/methods/reminders.delete func (api *Client) DeleteReminder(id string) error { + return api.DeleteReminderContext(context.Background(), id) +} + +// DeleteReminderContext deletes an existing reminder with a custom context +// +// For more details, see DeleteReminder documentation. +func (api *Client) DeleteReminderContext(ctx context.Context, id string) error { values := url.Values{ "token": {api.token}, "reminder": {id}, } response := &SlackResponse{} - if err := api.postMethod(context.Background(), "reminders.delete", values, response); err != nil { + if err := api.postMethod(ctx, "reminders.delete", values, response); err != nil { return err } return response.Err() diff --git a/backend/vendor/github.com/slack-go/slack/slackutilsx/slackutilsx.go b/backend/vendor/github.com/slack-go/slack/slackutilsx/slackutilsx.go index 1f7b2b8c..d6c9c07e 100644 --- a/backend/vendor/github.com/slack-go/slack/slackutilsx/slackutilsx.go +++ b/backend/vendor/github.com/slack-go/slack/slackutilsx/slackutilsx.go @@ -50,10 +50,12 @@ func DetectChannelType(channelID string) ChannelType { } } +// initialize replacer only once (if needed) +var escapeReplacer = strings.NewReplacer("&", "&", "<", "<", ">", ">") + // EscapeMessage text func EscapeMessage(message string) string { - replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">") - return replacer.Replace(message) + return escapeReplacer.Replace(message) } // Retryable errors return true. diff --git a/backend/vendor/github.com/slack-go/slack/views.go b/backend/vendor/github.com/slack-go/slack/views.go index e3ee8815..a3a1bd05 100644 --- a/backend/vendor/github.com/slack-go/slack/views.go +++ b/backend/vendor/github.com/slack-go/slack/views.go @@ -98,7 +98,7 @@ func NewErrorsViewSubmissionResponse(errors map[string]string) *ViewSubmissionRe type ModalViewRequest struct { Type ViewType `json:"type"` - Title *TextBlockObject `json:"title"` + Title *TextBlockObject `json:"title,omitempty"` Blocks Blocks `json:"blocks"` Close *TextBlockObject `json:"close,omitempty"` Submit *TextBlockObject `json:"submit,omitempty"` diff --git a/backend/vendor/github.com/slack-go/slack/webhooks.go b/backend/vendor/github.com/slack-go/slack/webhooks.go index 97346e1c..15097f03 100644 --- a/backend/vendor/github.com/slack-go/slack/webhooks.go +++ b/backend/vendor/github.com/slack-go/slack/webhooks.go @@ -1,7 +1,10 @@ package slack import ( + "bytes" "context" + "encoding/json" + "fmt" "net/http" ) @@ -31,3 +34,24 @@ func PostWebhookContext(ctx context.Context, url string, msg *WebhookMessage) er func PostWebhookCustomHTTP(url string, httpClient *http.Client, msg *WebhookMessage) error { return PostWebhookCustomHTTPContext(context.Background(), url, httpClient, msg) } + +func PostWebhookCustomHTTPContext(ctx context.Context, url string, httpClient *http.Client, msg *WebhookMessage) error { + raw, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("marshal failed: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw)) + if err != nil { + return fmt.Errorf("failed new request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to post webhook: %w", err) + } + defer resp.Body.Close() + + return checkStatusCode(resp, discard{}) +} diff --git a/backend/vendor/github.com/slack-go/slack/webhooks_go112.go b/backend/vendor/github.com/slack-go/slack/webhooks_go112.go deleted file mode 100644 index 0eb539ac..00000000 --- a/backend/vendor/github.com/slack-go/slack/webhooks_go112.go +++ /dev/null @@ -1,34 +0,0 @@ -//go:build !go1.13 -// +build !go1.13 - -package slack - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" -) - -func PostWebhookCustomHTTPContext(ctx context.Context, url string, httpClient *http.Client, msg *WebhookMessage) error { - raw, err := json.Marshal(msg) - if err != nil { - return fmt.Errorf("marshal failed: %v", err) - } - - req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(raw)) - if err != nil { - return fmt.Errorf("failed new request: %v", err) - } - req = req.WithContext(ctx) - req.Header.Set("Content-Type", "application/json") - - resp, err := httpClient.Do(req) - if err != nil { - return fmt.Errorf("failed to post webhook: %v", err) - } - defer resp.Body.Close() - - return checkStatusCode(resp, discard{}) -} diff --git a/backend/vendor/github.com/slack-go/slack/webhooks_go113.go b/backend/vendor/github.com/slack-go/slack/webhooks_go113.go deleted file mode 100644 index 021eac01..00000000 --- a/backend/vendor/github.com/slack-go/slack/webhooks_go113.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build go1.13 -// +build go1.13 - -package slack - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" -) - -func PostWebhookCustomHTTPContext(ctx context.Context, url string, httpClient *http.Client, msg *WebhookMessage) error { - raw, err := json.Marshal(msg) - if err != nil { - return fmt.Errorf("marshal failed: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw)) - if err != nil { - return fmt.Errorf("failed new request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - - resp, err := httpClient.Do(req) - if err != nil { - return fmt.Errorf("failed to post webhook: %w", err) - } - defer resp.Body.Close() - - return checkStatusCode(resp, discard{}) -} diff --git a/backend/vendor/github.com/slack-go/slack/websocket_managed_conn.go b/backend/vendor/github.com/slack-go/slack/websocket_managed_conn.go index 5555c316..92536171 100644 --- a/backend/vendor/github.com/slack-go/slack/websocket_managed_conn.go +++ b/backend/vendor/github.com/slack-go/slack/websocket_managed_conn.go @@ -9,11 +9,11 @@ import ( "reflect" "time" - "github.com/slack-go/slack/internal/backoff" - "github.com/slack-go/slack/internal/misc" - "github.com/gorilla/websocket" + + "github.com/slack-go/slack/internal/backoff" "github.com/slack-go/slack/internal/errorsx" + "github.com/slack-go/slack/internal/misc" "github.com/slack-go/slack/internal/timex" ) diff --git a/backend/vendor/github.com/slack-go/slack/workflow_step.go b/backend/vendor/github.com/slack-go/slack/workflow_step.go new file mode 100644 index 00000000..bcc892c5 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/workflow_step.go @@ -0,0 +1,98 @@ +package slack + +import ( + "context" + "encoding/json" +) + +const VTWorkflowStep ViewType = "workflow_step" + +type ( + ConfigurationModalRequest struct { + ModalViewRequest + } + + WorkflowStepCompleteResponse struct { + WorkflowStepEditID string `json:"workflow_step_edit_id"` + Inputs *WorkflowStepInputs `json:"inputs,omitempty"` + Outputs *[]WorkflowStepOutput `json:"outputs,omitempty"` + } + + WorkflowStepInputElement struct { + Value string `json:"value"` + SkipVariableReplacement bool `json:"skip_variable_replacement"` + } + + WorkflowStepInputs map[string]WorkflowStepInputElement + + WorkflowStepOutput struct { + Name string `json:"name"` + Type string `json:"type"` + Label string `json:"label"` + } +) + +func NewConfigurationModalRequest(blocks Blocks, privateMetaData string, externalID string) *ConfigurationModalRequest { + return &ConfigurationModalRequest{ + ModalViewRequest{ + Type: VTWorkflowStep, + Title: nil, // slack configuration modal must not have a title! + Blocks: blocks, + PrivateMetadata: privateMetaData, + ExternalID: externalID, + }, + } +} + +func (api *Client) SaveWorkflowStepConfiguration(workflowStepEditID string, inputs *WorkflowStepInputs, outputs *[]WorkflowStepOutput) error { + return api.SaveWorkflowStepConfigurationContext(context.Background(), workflowStepEditID, inputs, outputs) +} + +func (api *Client) SaveWorkflowStepConfigurationContext(ctx context.Context, workflowStepEditID string, inputs *WorkflowStepInputs, outputs *[]WorkflowStepOutput) error { + // More information: https://api.slack.com/methods/workflows.updateStep + wscr := WorkflowStepCompleteResponse{ + WorkflowStepEditID: workflowStepEditID, + Inputs: inputs, + Outputs: outputs, + } + + endpoint := api.endpoint + "workflows.updateStep" + jsonData, err := json.Marshal(wscr) + if err != nil { + return err + } + + response := &SlackResponse{} + if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonData, response, api); err != nil { + return err + } + + if !response.Ok { + return response.Err() + } + + return nil +} + +func GetInitialOptionFromWorkflowStepInput(selection *SelectBlockElement, inputs *WorkflowStepInputs, options []*OptionBlockObject) (*OptionBlockObject, bool) { + if len(*inputs) == 0 { + return &OptionBlockObject{}, false + } + if len(options) == 0 { + return &OptionBlockObject{}, false + } + + if val, ok := (*inputs)[selection.ActionID]; ok { + if val.SkipVariableReplacement { + return &OptionBlockObject{}, false + } + + for _, option := range options { + if option.Value == val.Value { + return option, true + } + } + } + + return &OptionBlockObject{}, false +} diff --git a/backend/vendor/modules.txt b/backend/vendor/modules.txt index 26ee925b..97410090 100644 --- a/backend/vendor/modules.txt +++ b/backend/vendor/modules.txt @@ -95,6 +95,9 @@ github.com/go-pkgz/auth/middleware github.com/go-pkgz/auth/provider github.com/go-pkgz/auth/provider/sender github.com/go-pkgz/auth/token +# github.com/go-pkgz/email v0.2.1-0.20220428155644-a7e5d837d02f +## explicit; go 1.17 +github.com/go-pkgz/email # github.com/go-pkgz/expirable-cache v0.0.3 ## explicit; go 1.14 github.com/go-pkgz/expirable-cache @@ -109,6 +112,9 @@ github.com/go-pkgz/lcw/internal/cache # github.com/go-pkgz/lgr v0.10.4 ## explicit; go 1.15 github.com/go-pkgz/lgr +# github.com/go-pkgz/notify v0.0.0-20220429105545-bc5c80a54199 +## explicit; go 1.17 +github.com/go-pkgz/notify # github.com/go-pkgz/repeater v1.1.3 ## explicit; go 1.12 github.com/go-pkgz/repeater @@ -205,7 +211,7 @@ github.com/russross/blackfriday/v2 github.com/skip2/go-qrcode github.com/skip2/go-qrcode/bitset github.com/skip2/go-qrcode/reedsolomon -# github.com/slack-go/slack v0.10.2 +# github.com/slack-go/slack v0.10.3 ## explicit; go 1.16 github.com/slack-go/slack github.com/slack-go/slack/internal/backoff @@ -290,7 +296,7 @@ golang.org/x/crypto/scrypt ## explicit; go 1.12 golang.org/x/image/draw golang.org/x/image/math/f64 -# golang.org/x/net v0.0.0-20220412020605-290c469a71a5 +# golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 ## explicit; go 1.17 golang.org/x/net/context golang.org/x/net/context/ctxhttp