switch to go-pkgz/notify package: modules
This commit is contained in:
committed by
Umputun
parent
598d767791
commit
0560a893cf
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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])
|
||||
|
||||
+5
-3
@@ -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
|
||||
|
||||
+8
-4
@@ -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=
|
||||
|
||||
+15
@@ -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/
|
||||
+61
@@ -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
|
||||
+21
@@ -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.
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
# email sending library
|
||||
|
||||
[](https://github.com/go-pkgz/email/actions) [](https://coveralls.io/github/go-pkgz/email?branch=master) [](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("<html>some content, foo bar</html>",
|
||||
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.
|
||||
+347
@@ -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{}) {}
|
||||
+70
@@ -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
|
||||
}
|
||||
}
|
||||
+70
@@ -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
|
||||
+21
@@ -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.
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
# Notify
|
||||
|
||||
[](https://github.com/go-pkgz/notify/actions) [](https://coveralls.io/github/go-pkgz/notify?branch=master) [](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"<john@example.org>?subject=test-subj&from="Notifier"<notify@example.org>`
|
||||
- `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"<john@example.org>?subject=test-subj&from="Notifier"<notify@example.org>`,
|
||||
"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 `<b>` and `h4-h6` with `<i><b>` to preserve formatting.
|
||||
|
||||
If you want to post text into HTML tag like <a>text</a>, 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/<BOT_USERNAME>/?start=<TOKEN>`
|
||||
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.
|
||||
+132
@@ -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"<john@example.org>?subject=test-subj&from="Notifier"<notify@example.org>
|
||||
// - 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
|
||||
}
|
||||
+28
@@ -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)
|
||||
}
|
||||
+108
@@ -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")
|
||||
}
|
||||
+478
@@ -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 <b> and <i> 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("<b>")
|
||||
}
|
||||
if token.Type == html.EndTagToken {
|
||||
buff.WriteString("</b>")
|
||||
}
|
||||
case "h4", "h5", "h6":
|
||||
if token.Type == html.StartTagToken {
|
||||
buff.WriteString("<i><b>")
|
||||
}
|
||||
if token.Type == html.EndTagToken {
|
||||
buff.WriteString("</b></i>")
|
||||
}
|
||||
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)
|
||||
}
|
||||
+96
@@ -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
|
||||
}
|
||||
+5
-1
@@ -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
|
||||
}
|
||||
|
||||
+10
@@ -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 {
|
||||
|
||||
+6
@@ -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{
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+29
-31
@@ -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,
|
||||
|
||||
+44
-35
@@ -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)
|
||||
|
||||
+5
-1
@@ -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
|
||||
}
|
||||
|
||||
+32
-22
@@ -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 {
|
||||
|
||||
+10
-10
@@ -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)
|
||||
}
|
||||
|
||||
+11
-15
@@ -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
|
||||
}
|
||||
|
||||
+32
-4
@@ -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()
|
||||
|
||||
+4
-2
@@ -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.
|
||||
|
||||
+1
-1
@@ -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"`
|
||||
|
||||
+24
@@ -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{})
|
||||
}
|
||||
|
||||
-34
@@ -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{})
|
||||
}
|
||||
-33
@@ -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{})
|
||||
}
|
||||
+3
-3
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
+98
@@ -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
|
||||
}
|
||||
Vendored
+8
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user