Use go modules

* Make use of go modules
* Update nlopes/slack module
This commit is contained in:
erroneousboat 2019-05-25 18:24:36 +02:00
parent 6e75095daa
commit 10d54bb19c
81 changed files with 3152 additions and 1212 deletions

95
Gopkg.lock generated
View File

@ -1,95 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
digest = "1:592569a314f98130ac3085243fdbe46f278d3e54c95ce9e0bde9c6b908db82c4"
name = "github.com/0xAX/notificator"
packages = ["."]
pruneopts = "UT"
revision = "88d57ee9043ba88d6a62e437fa15dda1ca0d2b59"
[[projects]]
digest = "1:c2ee2bebf300b3c6d998802bdefe0422a65bcdcdd5c902e1ed518448c56e8f98"
name = "github.com/erroneousboat/termui"
packages = ["."]
pruneopts = "UT"
revision = "80f245cdfa0488883a3e8602bf3f0c8a3c889a22"
[[projects]]
digest = "1:cee8e8ac80df6373e7daa11baf1f98c1b6f7242c49ccae7e1ec34a971dc408d9"
name = "github.com/gorilla/websocket"
packages = ["."]
pruneopts = "UT"
revision = "3ff3320c2a1756a3691521efc290b4701575147c"
version = "v1.3.0"
[[projects]]
digest = "1:f614e627d47e1276989de725dc5e433504a8b5498850711c9d3fcec3bfa7c943"
name = "github.com/maruel/panicparse"
packages = ["stack"]
pruneopts = "UT"
revision = "785840568bdc7faa0dfb1cd6c643207f03271f64"
version = "v1.1.1"
[[projects]]
digest = "1:cdb899c199f907ac9fb50495ec71212c95cb5b0e0a8ee0800da0238036091033"
name = "github.com/mattn/go-runewidth"
packages = ["."]
pruneopts = "UT"
revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb"
version = "v0.0.3"
[[projects]]
branch = "master"
digest = "1:e68cd472b96cdf7c9f6971ac41bcc1d4d3b23d67c2a31d2399446e295bc88ae9"
name = "github.com/mitchellh/go-wordwrap"
packages = ["."]
pruneopts = "UT"
revision = "ad45545899c7b13c020ea92b2072220eefad42b8"
[[projects]]
branch = "master"
digest = "1:410e126b7e96640ac0c41bb49bad7dbf2d1c081aa06fd2c75cdb9e65765fae9b"
name = "github.com/nlopes/slack"
packages = ["."]
pruneopts = "UT"
revision = "7cfa5619e6becd3db5dfb8e26c06798918e123b2"
[[projects]]
branch = "master"
digest = "1:f335d800550786b6f51ddaedb9d1107a7a72f4a2195e5b039dd7c0e103e119bc"
name = "github.com/nsf/termbox-go"
packages = ["."]
pruneopts = "UT"
revision = "b66b20ab708e289ff1eb3e218478302e6aec28ce"
[[projects]]
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = "UT"
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
digest = "1:3fd3d634f6815f19ac4b2c5e16d28ec9aa4584d0bba25d1ee6c424d813cca22a"
name = "github.com/renstrom/fuzzysearch"
packages = ["fuzzy"]
pruneopts = "UT"
revision = "b18e754edff4833912ef4dce9eaca885bd3f0de1"
version = "v1.0.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/0xAX/notificator",
"github.com/erroneousboat/termui",
"github.com/mattn/go-runewidth",
"github.com/nlopes/slack",
"github.com/nsf/termbox-go",
"github.com/renstrom/fuzzysearch/fuzzy",
]
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -1,50 +0,0 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/erroneousboat/termui"
revision = "80f245cdfa0488883a3e8602bf3f0c8a3c889a22"
[[constraint]]
name = "github.com/mattn/go-runewidth"
version = "0.0.2"
[[constraint]]
name = "github.com/nlopes/slack"
branch = "master"
[[constraint]]
name = "github.com/nsf/termbox-go"
branch = "master"
[[constraint]]
name = "github.com/renstrom/fuzzysearch"
version = "1.0.0"
[prune]
go-tests = true
unused-packages = true

View File

@ -22,6 +22,10 @@ dev: build
# We're setting the OS to linux (in case someone builds the binary on Mac or
# Windows)
#
# `-mod=vendor`
# This ensures that the build process will use the modules in the vendor
# folder.
#
# `-a`
# Force rebuilding of package, all import will be rebuilt with cgo disabled,
# which means all the imports will be rebuilt with cgo disabled.
@ -39,17 +43,20 @@ dev: build
# Location of the source files
build:
@ echo "+ $@"
@ CGO_ENABLED=0 go build -a -installsuffix cgo -o ./bin/slack-term .
@ go mod vendor
@ CGO_ENABLED=0 go build -mod=vendor -a -installsuffix cgo -o ./bin/slack-term .
# Cross-compile
# http://dave.cheney.net/2015/08/22/cross-compilation-with-go-1-5
build-linux:
@ echo "+ $@"
@ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -a -installsuffix cgo -o ./bin/slack-term-linux-amd64 .
@ go mod vendor
@ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -mod=vendor -a -installsuffix cgo -o ./bin/slack-term-linux-amd64 .
build-mac:
@ echo "+ $@"
@ GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -a -installsuffix cgo -o ./bin/slack-term-darwin-amd64 .
@ go mod vendor
@ GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -mod=vendor -a -installsuffix cgo -o ./bin/slack-term-darwin-amd64 .
run: build
@ echo "+ $@"

17
go.mod Normal file
View File

@ -0,0 +1,17 @@
module github.com/erroneousboat/slack-term
go 1.12
require (
github.com/0xAX/notificator v0.0.0-20171022182052-88d57ee9043b
github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04
github.com/gorilla/websocket v1.4.0 // indirect
github.com/maruel/panicparse v1.1.1 // indirect
github.com/mattn/go-runewidth v0.0.3
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/nlopes/slack v0.5.1-0.20190515005541-e2954b1409b0
github.com/nsf/termbox-go v0.0.0-20180819125858-b66b20ab708e
github.com/pkg/errors v0.8.1 // indirect
github.com/renstrom/fuzzysearch v1.0.1
github.com/stretchr/testify v1.3.0 // indirect
)

28
go.sum Normal file
View File

@ -0,0 +1,28 @@
github.com/0xAX/notificator v0.0.0-20171022182052-88d57ee9043b h1:Sn+u6zpXFyfm2X7ruh+z6SJiUVyFg8YElh6HIOhrRCA=
github.com/0xAX/notificator v0.0.0-20171022182052-88d57ee9043b/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04 h1:DaFwoQC0Neeb2y2CVFxDPrS1BeyWAkKc4VVBDTZ0N98=
github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04/go.mod h1:UPpsbgDrqmUayOFOkCGD7+xrBIml/1dA0dsqTRnZqac=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/maruel/panicparse v1.1.1 h1:k62YPcEoLncEEpjMt92GtG5ugb8WL/510Ys3/h5IkRc=
github.com/maruel/panicparse v1.1.1/go.mod h1:nty42YY5QByNC5MM7q/nj938VbgPU7avs45z6NClpxI=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/nlopes/slack v0.5.1-0.20190515005541-e2954b1409b0 h1:M89l6W6BKfWSn13BtDUcDeePI5bQE0frdE1a/n2bMc8=
github.com/nlopes/slack v0.5.1-0.20190515005541-e2954b1409b0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk=
github.com/nsf/termbox-go v0.0.0-20180819125858-b66b20ab708e h1:fvw0uluMptljaRKSU8459cJ4bmi3qUYyMs5kzpic2fY=
github.com/nsf/termbox-go v0.0.0-20180819125858-b66b20ab708e/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/renstrom/fuzzysearch v1.0.1 h1:hnh2Fhqqa5I41Xgmm7UMAYgEIRn/iZwWItfwUHr1IWE=
github.com/renstrom/fuzzysearch v1.0.1/go.mod h1:SAEjPB4voP88qmWJXI7mA5m15uNlEnuHLx4Eu2mPGpQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

View File

@ -294,14 +294,16 @@ func (s *SlackService) MarkAsRead(channelItem components.ChannelItem) {
func (s *SlackService) SendMessage(channelID string, message string) error {
// https://godoc.org/github.com/nlopes/slack#PostMessageParameters
postParams := slack.PostMessageParameters{
postParams := slack.MsgOptionPostMessageParameters(slack.PostMessageParameters{
AsUser: true,
Username: s.CurrentUsername,
LinkNames: 1,
}
})
text := slack.MsgOptionText(message, true)
// https://godoc.org/github.com/nlopes/slack#Client.PostMessage
_, _, err := s.Client.PostMessage(channelID, message, postParams)
_, _, err := s.Client.PostMessage(channelID, text, postParams)
if err != nil {
return err
}
@ -314,15 +316,17 @@ func (s *SlackService) SendMessage(channelID string, message string) error {
// https://api.slack.com/docs/message-threading, 'Posting replies')
func (s *SlackService) SendReply(channelID string, threadID string, message string) error {
// https://godoc.org/github.com/nlopes/slack#PostMessageParameters
postParams := slack.PostMessageParameters{
postParams := slack.MsgOptionPostMessageParameters(slack.PostMessageParameters{
AsUser: true,
Username: s.CurrentUsername,
LinkNames: 1,
ThreadTimestamp: threadID,
}
})
text := slack.MsgOptionText(message, true)
// https://godoc.org/github.com/nlopes/slack#Client.PostMessage
_, _, err := s.Client.PostMessage(channelID, message, postParams)
_, _, err := s.Client.PostMessage(channelID, text, postParams)
if err != nil {
return err
}

View File

@ -3,13 +3,11 @@ sudo: false
matrix:
include:
- go: 1.4
- go: 1.5.x
- go: 1.6.x
- go: 1.7.x
- go: 1.8.x
- go: 1.9.x
- go: 1.10.x
- go: 1.11.x
- go: tip
allow_failures:
- go: tip

View File

@ -6,12 +6,14 @@ package websocket
import (
"bytes"
"context"
"crypto/tls"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptrace"
"net/url"
"strings"
"time"
@ -51,6 +53,10 @@ type Dialer struct {
// NetDial is nil, net.Dial is used.
NetDial func(network, addr string) (net.Conn, error)
// NetDialContext specifies the dial function for creating TCP connections. If
// NetDialContext is nil, net.DialContext is used.
NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error)
// Proxy specifies a function to return a proxy for a given
// Request. If the function returns a non-nil error, the
// request is aborted with the provided error.
@ -69,6 +75,17 @@ type Dialer struct {
// do not limit the size of the messages that can be sent or received.
ReadBufferSize, WriteBufferSize int
// WriteBufferPool is a pool of buffers for write operations. If the value
// is not set, then write buffers are allocated to the connection for the
// lifetime of the connection.
//
// A pool is most useful when the application has a modest volume of writes
// across a large number of connections.
//
// Applications should use a single pool for each unique value of
// WriteBufferSize.
WriteBufferPool BufferPool
// Subprotocols specifies the client's requested subprotocols.
Subprotocols []string
@ -84,6 +101,11 @@ type Dialer struct {
Jar http.CookieJar
}
// Dial creates a new client connection by calling DialContext with a background context.
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
return d.DialContext(context.Background(), urlStr, requestHeader)
}
var errMalformedURL = errors.New("malformed ws or wss URL")
func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
@ -111,19 +133,20 @@ var DefaultDialer = &Dialer{
}
// nilDialer is dialer to use when receiver is nil.
var nilDialer Dialer = *DefaultDialer
var nilDialer = *DefaultDialer
// Dial creates a new client connection. Use requestHeader to specify the
// DialContext creates a new client connection. Use requestHeader to specify the
// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
// Use the response.Header to get the selected subprotocol
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
//
// The context will be used in the request and in the Dialer
//
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
// non-nil *http.Response so that callers can handle redirects, authentication,
// etcetera. The response body may not contain the entire response and does not
// need to be closed by the application.
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
if d == nil {
d = &nilDialer
}
@ -161,6 +184,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
Header: make(http.Header),
Host: u.Host,
}
req = req.WithContext(ctx)
// Set the cookies present in the cookie jar of the dialer
if d.Jar != nil {
@ -204,20 +228,30 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"}
}
var deadline time.Time
if d.HandshakeTimeout != 0 {
deadline = time.Now().Add(d.HandshakeTimeout)
var cancel func()
ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout)
defer cancel()
}
// Get network dial function.
netDial := d.NetDial
if netDial == nil {
netDialer := &net.Dialer{Deadline: deadline}
netDial = netDialer.Dial
var netDial func(network, add string) (net.Conn, error)
if d.NetDialContext != nil {
netDial = func(network, addr string) (net.Conn, error) {
return d.NetDialContext(ctx, network, addr)
}
} else if d.NetDial != nil {
netDial = d.NetDial
} else {
netDialer := &net.Dialer{}
netDial = func(network, addr string) (net.Conn, error) {
return netDialer.DialContext(ctx, network, addr)
}
}
// If needed, wrap the dial function to set the connection deadline.
if !deadline.Equal(time.Time{}) {
if deadline, ok := ctx.Deadline(); ok {
forwardDial := netDial
netDial = func(network, addr string) (net.Conn, error) {
c, err := forwardDial(network, addr)
@ -249,7 +283,17 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
}
hostPort, hostNoPort := hostPortNoPort(u)
trace := httptrace.ContextClientTrace(ctx)
if trace != nil && trace.GetConn != nil {
trace.GetConn(hostPort)
}
netConn, err := netDial("tcp", hostPort)
if trace != nil && trace.GotConn != nil {
trace.GotConn(httptrace.GotConnInfo{
Conn: netConn,
})
}
if err != nil {
return nil, nil, err
}
@ -267,22 +311,31 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
}
tlsConn := tls.Client(netConn, cfg)
netConn = tlsConn
if err := tlsConn.Handshake(); err != nil {
return nil, nil, err
var err error
if trace != nil {
err = doHandshakeWithTrace(trace, tlsConn, cfg)
} else {
err = doHandshake(tlsConn, cfg)
}
if !cfg.InsecureSkipVerify {
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
return nil, nil, err
}
if err != nil {
return nil, nil, err
}
}
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize)
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil)
if err := req.Write(netConn); err != nil {
return nil, nil, err
}
if trace != nil && trace.GotFirstResponseByte != nil {
if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 {
trace.GotFirstResponseByte()
}
}
resp, err := http.ReadResponse(conn.br, req)
if err != nil {
return nil, nil, err
@ -328,3 +381,15 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
netConn = nil // to avoid close in defer.
return conn, resp, nil
}
func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error {
if err := tlsConn.Handshake(); err != nil {
return err
}
if !cfg.InsecureSkipVerify {
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
return err
}
}
return nil
}

View File

@ -223,6 +223,20 @@ func isValidReceivedCloseCode(code int) bool {
return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999)
}
// BufferPool represents a pool of buffers. The *sync.Pool type satisfies this
// interface. The type of the value stored in a pool is not specified.
type BufferPool interface {
// Get gets a value from the pool or returns nil if the pool is empty.
Get() interface{}
// Put adds a value to the pool.
Put(interface{})
}
// writePoolData is the type added to the write buffer pool. This wrapper is
// used to prevent applications from peeking at and depending on the values
// added to the pool.
type writePoolData struct{ buf []byte }
// The Conn type represents a WebSocket connection.
type Conn struct {
conn net.Conn
@ -232,6 +246,8 @@ type Conn struct {
// Write fields
mu chan bool // used as mutex to protect write to conn
writeBuf []byte // frame is constructed in this buffer.
writePool BufferPool
writeBufSize int
writeDeadline time.Time
writer io.WriteCloser // the current writer returned to the application
isWriting bool // for best-effort concurrent write detection
@ -263,64 +279,29 @@ type Conn struct {
newDecompressionReader func(io.Reader) io.ReadCloser
}
func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn {
return newConnBRW(conn, isServer, readBufferSize, writeBufferSize, nil)
}
func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn {
type writeHook struct {
p []byte
}
func (wh *writeHook) Write(p []byte) (int, error) {
wh.p = p
return len(p), nil
}
func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, brw *bufio.ReadWriter) *Conn {
mu := make(chan bool, 1)
mu <- true
var br *bufio.Reader
if readBufferSize == 0 && brw != nil && brw.Reader != nil {
// Reuse the supplied bufio.Reader if the buffer has a useful size.
// This code assumes that peek on a reader returns
// bufio.Reader.buf[:0].
brw.Reader.Reset(conn)
if p, err := brw.Reader.Peek(0); err == nil && cap(p) >= 256 {
br = brw.Reader
}
}
if br == nil {
if readBufferSize == 0 {
readBufferSize = defaultReadBufferSize
}
if readBufferSize < maxControlFramePayloadSize {
} else if readBufferSize < maxControlFramePayloadSize {
// must be large enough for control frame
readBufferSize = maxControlFramePayloadSize
}
br = bufio.NewReaderSize(conn, readBufferSize)
}
var writeBuf []byte
if writeBufferSize == 0 && brw != nil && brw.Writer != nil {
// Use the bufio.Writer's buffer if the buffer has a useful size. This
// code assumes that bufio.Writer.buf[:1] is passed to the
// bufio.Writer's underlying writer.
var wh writeHook
brw.Writer.Reset(&wh)
brw.Writer.WriteByte(0)
brw.Flush()
if cap(wh.p) >= maxFrameHeaderSize+256 {
writeBuf = wh.p[:cap(wh.p)]
}
}
if writeBuf == nil {
if writeBufferSize == 0 {
writeBufferSize = defaultWriteBufferSize
}
writeBuf = make([]byte, writeBufferSize+maxFrameHeaderSize)
if writeBufferSize <= 0 {
writeBufferSize = defaultWriteBufferSize
}
writeBufferSize += maxFrameHeaderSize
if writeBuf == nil && writeBufferPool == nil {
writeBuf = make([]byte, writeBufferSize)
}
mu := make(chan bool, 1)
mu <- true
c := &Conn{
isServer: isServer,
br: br,
@ -328,6 +309,8 @@ func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize in
mu: mu,
readFinal: true,
writeBuf: writeBuf,
writePool: writeBufferPool,
writeBufSize: writeBufferSize,
enableWriteCompression: true,
compressionLevel: defaultCompressionLevel,
}
@ -370,6 +353,15 @@ func (c *Conn) writeFatal(err error) error {
return err
}
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
c.br.Discard(len(p))
return p, err
}
func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error {
<-c.mu
defer func() { c.mu <- true }()
@ -475,7 +467,19 @@ func (c *Conn) prepWrite(messageType int) error {
c.writeErrMu.Lock()
err := c.writeErr
c.writeErrMu.Unlock()
return err
if err != nil {
return err
}
if c.writeBuf == nil {
wpd, ok := c.writePool.Get().(writePoolData)
if ok {
c.writeBuf = wpd.buf
} else {
c.writeBuf = make([]byte, c.writeBufSize)
}
}
return nil
}
// NextWriter returns a writer for the next message to send. The writer's Close
@ -601,6 +605,10 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error {
if final {
c.writer = nil
if c.writePool != nil {
c.writePool.Put(writePoolData{buf: c.writeBuf})
c.writeBuf = nil
}
return nil
}

View File

@ -1,18 +0,0 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.5
package websocket
import "io"
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
c.br.Discard(len(p))
return p, err
}

View File

@ -1,21 +0,0 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.5
package websocket
import "io"
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
if len(p) > 0 {
// advance over the bytes just read
io.ReadFull(c.br, p)
}
return p, err
}

View File

@ -19,7 +19,6 @@ import (
type PreparedMessage struct {
messageType int
data []byte
err error
mu sync.Mutex
frames map[prepareKey]*preparedFrame
}

View File

@ -7,7 +7,7 @@ package websocket
import (
"bufio"
"errors"
"net"
"io"
"net/http"
"net/url"
"strings"
@ -33,6 +33,17 @@ type Upgrader struct {
// or received.
ReadBufferSize, WriteBufferSize int
// WriteBufferPool is a pool of buffers for write operations. If the value
// is not set, then write buffers are allocated to the connection for the
// lifetime of the connection.
//
// A pool is most useful when the application has a modest volume of writes
// across a large number of connections.
//
// Applications should use a single pool for each unique value of
// WriteBufferSize.
WriteBufferPool BufferPool
// Subprotocols specifies the server's supported protocols in order of
// preference. If this field is not nil, then the Upgrade method negotiates a
// subprotocol by selecting the first match in this list with a protocol
@ -159,17 +170,12 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
}
}
var (
netConn net.Conn
err error
)
h, ok := w.(http.Hijacker)
if !ok {
return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")
}
var brw *bufio.ReadWriter
netConn, brw, err = h.Hijack()
netConn, brw, err := h.Hijack()
if err != nil {
return u.returnError(w, r, http.StatusInternalServerError, err.Error())
}
@ -179,7 +185,21 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
return nil, errors.New("websocket: client sent data before handshake is complete")
}
c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw)
var br *bufio.Reader
if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 {
// Reuse hijacked buffered reader as connection reader.
br = brw.Reader
}
buf := bufioWriterBuffer(netConn, brw.Writer)
var writeBuf []byte
if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 {
// Reuse hijacked write buffer as connection buffer.
writeBuf = buf
}
c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf)
c.subprotocol = subprotocol
if compress {
@ -187,7 +207,13 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
c.newDecompressionReader = decompressNoContextTakeover
}
p := c.writeBuf[:0]
// Use larger of hijacked buffer and connection write buffer for header.
p := buf
if len(c.writeBuf) > len(p) {
p = c.writeBuf
}
p = p[:0]
p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
p = append(p, computeAcceptKey(challengeKey)...)
p = append(p, "\r\n"...)
@ -298,3 +324,40 @@ func IsWebSocketUpgrade(r *http.Request) bool {
return tokenListContainsValue(r.Header, "Connection", "upgrade") &&
tokenListContainsValue(r.Header, "Upgrade", "websocket")
}
// bufioReaderSize size returns the size of a bufio.Reader.
func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int {
// This code assumes that peek on a reset reader returns
// bufio.Reader.buf[:0].
// TODO: Use bufio.Reader.Size() after Go 1.10
br.Reset(originalReader)
if p, err := br.Peek(0); err == nil {
return cap(p)
}
return 0
}
// writeHook is an io.Writer that records the last slice passed to it vio
// io.Writer.Write.
type writeHook struct {
p []byte
}
func (wh *writeHook) Write(p []byte) (int, error) {
wh.p = p
return len(p), nil
}
// bufioWriterBuffer grabs the buffer from a bufio.Writer.
func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte {
// This code assumes that bufio.Writer.buf[:1] is passed to the
// bufio.Writer's underlying writer.
var wh writeHook
bw.Reset(&wh)
bw.WriteByte(0)
bw.Flush()
bw.Reset(originalWriter)
return wh.p[:cap(wh.p)]
}

19
vendor/github.com/gorilla/websocket/trace.go generated vendored Normal file
View File

@ -0,0 +1,19 @@
// +build go1.8
package websocket
import (
"crypto/tls"
"net/http/httptrace"
)
func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error {
if trace.TLSHandshakeStart != nil {
trace.TLSHandshakeStart()
}
err := doHandshake(tlsConn, cfg)
if trace.TLSHandshakeDone != nil {
trace.TLSHandshakeDone(tlsConn.ConnectionState(), err)
}
return err
}

12
vendor/github.com/gorilla/websocket/trace_17.go generated vendored Normal file
View File

@ -0,0 +1,12 @@
// +build !go1.8
package websocket
import (
"crypto/tls"
"net/http/httptrace"
)
func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error {
return doHandshake(tlsConn, cfg)
}

View File

@ -178,7 +178,7 @@ headers:
return false
}
// parseExtensiosn parses WebSocket extensions from a header.
// parseExtensions parses WebSocket extensions from a header.
func parseExtensions(header http.Header) []map[string]string {
// From RFC 6455:
//

14
vendor/github.com/nlopes/slack/.gometalinter.json generated vendored Normal file
View File

@ -0,0 +1,14 @@
{
"DisableAll": true,
"Enable": [
"structcheck",
"vet",
"misspell",
"unconvert",
"interfacer",
"goimports"
],
"Vendor": true,
"Exclude": ["vendor"],
"Deadline": "300s"
}

View File

@ -1,21 +1,35 @@
language: go
go:
- 1.7.x
- 1.8.x
- 1.9.x
- tip
env:
- GO111MODULE=on
install: true
before_install:
- export PATH=$HOME/gopath/bin:$PATH
# install gometalinter
- curl -L https://git.io/vp6lP | sh
script:
- go test -race ./...
- go test -cover ./...
- PATH=$PWD/bin:$PATH gometalinter ./...
- go test -race -cover ./...
matrix:
allow_failures:
- go: tip
allow_failures:
- go: tip
include:
- go: "1.7.x"
script: go test -v ./...
- go: "1.8.x"
script: go test -v ./...
- go: "1.9.x"
script: go test -v ./...
- go: "1.10.x"
script: go test -v ./...
- go: "1.11.x"
script: go test -v -mod=vendor ./...
- go: "tip"
script: go test -v -mod=vendor ./...
git:
depth: 10

View File

@ -1,3 +1,20 @@
### v0.5.0 - January 20, 2019
full differences can be viewed using `git log --oneline --decorate --color v0.4.0..v0.5.0`
- Breaking changes: various old struct fields have been removed or updated to match slack's api.
- deadlock fix in RTM disconnect.
### v0.4.0 - October 06, 2018
full differences can be viewed using `git log --oneline --decorate --color v0.3.0..v0.4.0`
- Breaking Change: renamed ApplyMessageOption, to mark it as unsafe,
this means it may break without warning in the future.
- Breaking: Msg structure files field changed to an array.
- General: implementation for new security headers.
- RTM: deadlock fix between connect/disconnect.
- Events: various new fields added.
- Web: various fixes, new fields exposed, new methods added.
- Interactions: minor additions expect breaking changes in next release for dialogs/button clicks.
- Utils: new methods added.
### v0.3.0 - July 30, 2018
full differences can be viewed using `git log --oneline --decorate --color v0.2.0..v0.3.0`
- slack events initial support added. (still considered experimental and undergoing changes, stability not promised)

View File

@ -1,33 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
name = "github.com/gorilla/websocket"
packages = ["."]
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/stretchr/testify"
packages = ["assert"]
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "888307bf47ee004aaaa4c45e6139929b4984f2253e48e382246bfb8c66f3cd65"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -1,13 +0,0 @@
ignored = ["github.com/lusis/slack-test"]
[[constraint]]
name = "github.com/gorilla/websocket"
version = "1.2.0"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"
[prune]
go-tests = true
unused-packages = true

View File

@ -9,18 +9,10 @@ a fully managed way.
## Change log
Support for the EventsAPI has recently been added. It is still in its early stages but nearly all events have been added and tested (except for those events in [Developer Preview](https://api.slack.com/slack-apps-preview) mode). API stability for events is not promised at this time.
### v0.2.0 - Feb 10, 2018
## Changelog
Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against.
Please check [0.2.0](https://github.com/nlopes/slack/releases/tag/v0.2.0)
### CHANGELOG.md
[CHANGELOG.md](https://github.com/nlopes/slack/blob/master/CHANGELOG.md) is available. Please visit it for updates.
[CHANGELOG.md](https://github.com/nlopes/slack/blob/master/CHANGELOG.md) is available. Please visit it for updates.
## Installing
@ -43,7 +35,7 @@ func main() {
api := slack.New("YOUR_TOKEN_HERE")
// If you set debugging, it will log all requests to the console
// Useful when encountering issues
// api.SetDebug(true)
// slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true))
groups, err := api.GetGroups(false)
if err != nil {
fmt.Printf("%s\n", err)

View File

@ -2,28 +2,19 @@ package slack
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
)
type adminResponse struct {
OK bool `json:"ok"`
Error string `json:"error"`
}
func adminRequest(ctx context.Context, client HTTPRequester, method string, teamName string, values url.Values, debug bool) (*adminResponse, error) {
adminResponse := &adminResponse{}
err := parseAdminResponse(ctx, client, method, teamName, values, adminResponse, debug)
func (api *Client) adminRequest(ctx context.Context, method string, teamName string, values url.Values) error {
resp := &SlackResponse{}
err := parseAdminResponse(ctx, api.httpclient, method, teamName, values, resp, api)
if err != nil {
return nil, err
return err
}
if !adminResponse.OK {
return nil, errors.New(adminResponse.Error)
}
return adminResponse, nil
return resp.Err()
}
// DisableUser disabled a user account, given a user ID
@ -40,9 +31,8 @@ func (api *Client) DisableUserContext(ctx context.Context, teamName string, uid
"_attempts": {"1"},
}
_, err := adminRequest(ctx, api.httpclient, "setInactive", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err)
if err := api.adminRequest(ctx, "setInactive", teamName, values); err != nil {
return fmt.Errorf("failed to disable user with id '%s': %s", uid, err)
}
return nil
@ -67,7 +57,7 @@ func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, fi
"_attempts": {"1"},
}
_, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug)
err := api.adminRequest(ctx, "invite", teamName, values)
if err != nil {
return fmt.Errorf("Failed to invite single-channel guest: %s", err)
}
@ -94,7 +84,7 @@ func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channe
"_attempts": {"1"},
}
_, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug)
err := api.adminRequest(ctx, "invite", teamName, values)
if err != nil {
return fmt.Errorf("Failed to restricted account: %s", err)
}
@ -118,7 +108,7 @@ func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName,
"_attempts": {"1"},
}
_, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug)
err := api.adminRequest(ctx, "invite", teamName, values)
if err != nil {
return fmt.Errorf("Failed to invite to team: %s", err)
}
@ -140,7 +130,7 @@ func (api *Client) SetRegularContext(ctx context.Context, teamName, user string)
"_attempts": {"1"},
}
_, err := adminRequest(ctx, api.httpclient, "setRegular", teamName, values, api.debug)
err := api.adminRequest(ctx, "setRegular", teamName, values)
if err != nil {
return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err)
}
@ -162,7 +152,7 @@ func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, use
"_attempts": {"1"},
}
_, err := adminRequest(ctx, api.httpclient, "sendSSOBind", teamName, values, api.debug)
err := api.adminRequest(ctx, "sendSSOBind", teamName, values)
if err != nil {
return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err)
}
@ -185,7 +175,7 @@ func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid,
"_attempts": {"1"},
}
_, err := adminRequest(ctx, api.httpclient, "setUltraRestricted", teamName, values, api.debug)
err := api.adminRequest(ctx, "setUltraRestricted", teamName, values)
if err != nil {
return fmt.Errorf("Failed to ultra-restrict account: %s", err)
}
@ -194,22 +184,23 @@ func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid,
}
// SetRestricted converts a user into a restricted account
func (api *Client) SetRestricted(teamName, uid string) error {
return api.SetRestrictedContext(context.Background(), teamName, uid)
func (api *Client) SetRestricted(teamName, uid string, channelIds ...string) error {
return api.SetRestrictedContext(context.Background(), teamName, uid, channelIds...)
}
// SetRestrictedContext converts a user into a restricted account with a custom context
func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid string) error {
func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid string, channelIds ...string) error {
values := url.Values{
"user": {uid},
"token": {api.token},
"set_active": {"true"},
"_attempts": {"1"},
"channels": {strings.Join(channelIds, ",")},
}
_, err := adminRequest(ctx, api.httpclient, "setRestricted", teamName, values, api.debug)
err := api.adminRequest(ctx, "setRestricted", teamName, values)
if err != nil {
return fmt.Errorf("Failed to restrict account: %s", err)
return fmt.Errorf("failed to restrict account: %s", err)
}
return nil

View File

@ -17,7 +17,7 @@ type AttachmentAction struct {
Name string `json:"name"` // Required.
Text string `json:"text"` // Required.
Style string `json:"style,omitempty"` // Optional. Allowed values: "default", "primary", "danger".
Type string `json:"type"` // Required. Must be set to "button" or "select".
Type actionType `json:"type"` // Required. Must be set to "button" or "select".
Value string `json:"value,omitempty"` // Optional.
DataSource string `json:"data_source,omitempty"` // Optional.
MinQueryLength int `json:"min_query_length,omitempty"` // Optional. Default value is 1.
@ -28,6 +28,11 @@ type AttachmentAction struct {
URL string `json:"url,omitempty"` // Optional.
}
// actionType returns the type of the action
func (a AttachmentAction) actionType() actionType {
return a.Type
}
// AttachmentActionOption the individual option to appear in action menu.
type AttachmentActionOption struct {
Text string `json:"text"` // Required.
@ -42,25 +47,8 @@ type AttachmentActionOptionGroup struct {
}
// AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction)
type AttachmentActionCallback struct {
Actions []AttachmentAction `json:"actions"`
CallbackID string `json:"callback_id"`
Team Team `json:"team"`
Channel Channel `json:"channel"`
User User `json:"user"`
Name string `json:"name"`
Value string `json:"value"`
OriginalMessage Message `json:"original_message"`
ActionTs string `json:"action_ts"`
MessageTs string `json:"message_ts"`
AttachmentID string `json:"attachment_id"`
Token string `json:"token"`
ResponseURL string `json:"response_url"`
TriggerID string `json:"trigger_id"`
}
// DEPRECATED: use InteractionCallback
type AttachmentActionCallback InteractionCallback
// ConfirmationField are used to ask users to confirm actions
type ConfirmationField struct {

40
vendor/github.com/nlopes/slack/auth.go generated vendored Normal file
View File

@ -0,0 +1,40 @@
package slack
import (
"context"
"net/url"
)
// AuthRevokeResponse contains our Auth response from the auth.revoke endpoint
type AuthRevokeResponse struct {
SlackResponse // Contains the "ok", and "Error", if any
Revoked bool `json:"revoked,omitempty"`
}
// authRequest sends the actual request, and unmarshals the response
func (api *Client) authRequest(ctx context.Context, path string, values url.Values) (*AuthRevokeResponse, error) {
response := &AuthRevokeResponse{}
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
return response, response.Err()
}
// SendAuthRevoke will send a revocation for our token
func (api *Client) SendAuthRevoke(token string) (*AuthRevokeResponse, error) {
return api.SendAuthRevokeContext(context.Background(), token)
}
// SendAuthRevokeContext will retrieve the satus from api.test
func (api *Client) SendAuthRevokeContext(ctx context.Context, token string) (*AuthRevokeResponse, error) {
if token == "" {
token = api.token
}
values := url.Values{
"token": {token},
}
return api.authRequest(ctx, "auth.revoke", values)
}

View File

@ -1,7 +1,6 @@
package slack
import (
"math"
"math/rand"
"time"
)
@ -14,41 +13,42 @@ import (
// conjunction with the time package.
type backoff struct {
attempts int
//Factor is the multiplying factor for each increment step
Factor float64
//Jitter eases contention by randomizing backoff steps
Jitter bool
//Min and Max are the minimum and maximum values of the counter
Min, Max time.Duration
// Initial value to scale out
Initial time.Duration
// Jitter value randomizes an additional delay between 0 and Jitter
Jitter time.Duration
// Max maximum values of the backoff
Max time.Duration
}
// Returns the current value of the counter and then multiplies it
// Factor
func (b *backoff) Duration() time.Duration {
//Zero-values are nonsensical, so we use
//them to apply defaults
if b.Min == 0 {
b.Min = 100 * time.Millisecond
}
func (b *backoff) Duration() (dur time.Duration) {
// Zero-values are nonsensical, so we use
// them to apply defaults
if b.Max == 0 {
b.Max = 10 * time.Second
}
if b.Factor == 0 {
b.Factor = 2
if b.Initial == 0 {
b.Initial = 100 * time.Millisecond
}
//calculate this duration
dur := float64(b.Min) * math.Pow(b.Factor, float64(b.attempts))
if b.Jitter {
dur = rand.Float64()*(dur-float64(b.Min)) + float64(b.Min)
// calculate this duration
if dur = time.Duration(1 << uint(b.attempts)); dur > 0 {
dur = dur * b.Initial
} else {
dur = b.Max
}
//cap!
if dur > float64(b.Max) {
return b.Max
if b.Jitter > 0 {
dur = dur + time.Duration(rand.Intn(int(b.Jitter)))
}
//bump attempts count
// bump attempts count
b.attempts++
//return as a time.Duration
return time.Duration(dur)
return dur
}
//Resets the current value of the counter back to Min

61
vendor/github.com/nlopes/slack/block.go generated vendored Normal file
View File

@ -0,0 +1,61 @@
package slack
// @NOTE: Blocks are in beta and subject to change.
// More Information: https://api.slack.com/block-kit
// MessageBlockType defines a named string type to define each block type
// as a constant for use within the package.
type MessageBlockType string
const (
MBTSection MessageBlockType = "section"
MBTDivider MessageBlockType = "divider"
MBTImage MessageBlockType = "image"
MBTAction MessageBlockType = "actions"
MBTContext MessageBlockType = "context"
)
// Block defines an interface all block types should implement
// to ensure consistency between blocks.
type Block interface {
BlockType() MessageBlockType
}
// Blocks is a convenience struct defined to allow dynamic unmarshalling of
// the "blocks" value in Slack's JSON response, which varies depending on block type
type Blocks struct {
BlockSet []Block `json:"blocks,omitempty"`
}
// BlockAction is the action callback sent when a block is interacted with
type BlockAction struct {
ActionID string `json:"action_id"`
BlockID string `json:"block_id"`
Text TextBlockObject `json:"text"`
Value string `json:"value"`
Type actionType `json:"type"`
ActionTs string `json:"action_ts"`
}
// actionType returns the type of the action
func (b BlockAction) actionType() actionType {
return b.Type
}
// NewBlockMessage creates a new Message that contains one or more blocks to be displayed
func NewBlockMessage(blocks ...Block) Message {
return Message{
Msg: Msg{
Blocks: Blocks{
BlockSet: blocks,
},
},
}
}
// AddBlockMessage appends a block to the end of the existing list of blocks
func AddBlockMessage(message Message, newBlk Block) Message {
message.Msg.Blocks.BlockSet = append(message.Msg.Blocks.BlockSet, newBlk)
return message
}

26
vendor/github.com/nlopes/slack/block_action.go generated vendored Normal file
View File

@ -0,0 +1,26 @@
package slack
// ActionBlock defines data that is used to hold interactive elements.
//
// More Information: https://api.slack.com/reference/messaging/blocks#actions
type ActionBlock struct {
Type MessageBlockType `json:"type"`
BlockID string `json:"block_id,omitempty"`
Elements BlockElements `json:"elements"`
}
// BlockType returns the type of the block
func (s ActionBlock) BlockType() MessageBlockType {
return s.Type
}
// NewActionBlock returns a new instance of an Action Block
func NewActionBlock(blockID string, elements ...BlockElement) *ActionBlock {
return &ActionBlock{
Type: MBTAction,
BlockID: blockID,
Elements: BlockElements{
ElementSet: elements,
},
}
}

32
vendor/github.com/nlopes/slack/block_context.go generated vendored Normal file
View File

@ -0,0 +1,32 @@
package slack
// ContextBlock defines data that is used to display message context, which can
// include both images and text.
//
// More Information: https://api.slack.com/reference/messaging/blocks#actions
type ContextBlock struct {
Type MessageBlockType `json:"type"`
BlockID string `json:"block_id,omitempty"`
ContextElements ContextElements `json:"elements"`
}
// BlockType returns the type of the block
func (s ContextBlock) BlockType() MessageBlockType {
return s.Type
}
type ContextElements struct {
Elements []MixedElement
}
// NewContextBlock returns a new instance of a context block
func NewContextBlock(blockID string, mixedElements ...MixedElement) *ContextBlock {
elements := ContextElements{
Elements: mixedElements,
}
return &ContextBlock{
Type: MBTContext,
BlockID: blockID,
ContextElements: elements,
}
}

303
vendor/github.com/nlopes/slack/block_conv.go generated vendored Normal file
View File

@ -0,0 +1,303 @@
package slack
import (
"encoding/json"
"github.com/pkg/errors"
)
type sumtype struct {
TypeVal string `json:"type"`
}
// MarshalJSON implements the Marshaller interface for Blocks so that any JSON
// marshalling is delegated and proper type determination can be made before marshal
func (b Blocks) MarshalJSON() ([]byte, error) {
bytes, err := json.Marshal(b.BlockSet)
if err != nil {
return nil, err
}
return bytes, nil
}
// UnmarshalJSON implements the Unmarshaller interface for Blocks, so that any JSON
// unmarshalling is delegated and proper type determination can be made before unmarshal
func (b *Blocks) UnmarshalJSON(data []byte) error {
var raw []json.RawMessage
if string(data) == "{}" {
return nil
}
err := json.Unmarshal(data, &raw)
if err != nil {
return err
}
var blocks Blocks
for _, r := range raw {
s := sumtype{}
err := json.Unmarshal(r, &s)
if err != nil {
return err
}
var blockType string
if s.TypeVal != "" {
blockType = s.TypeVal
}
var block Block
switch blockType {
case "actions":
block = &ActionBlock{}
case "context":
block = &ContextBlock{}
case "divider":
block = &DividerBlock{}
case "image":
block = &ImageBlock{}
case "section":
block = &SectionBlock{}
default:
return errors.New("unsupported block type")
}
err = json.Unmarshal(r, block)
if err != nil {
return err
}
blocks.BlockSet = append(blocks.BlockSet, block)
}
*b = blocks
return nil
}
// MarshalJSON implements the Marshaller interface for BlockElements so that any JSON
// marshalling is delegated and proper type determination can be made before marshal
func (b *BlockElements) MarshalJSON() ([]byte, error) {
bytes, err := json.Marshal(b.ElementSet)
if err != nil {
return nil, err
}
return bytes, nil
}
// UnmarshalJSON implements the Unmarshaller interface for BlockElements, so that any JSON
// unmarshalling is delegated and proper type determination can be made before unmarshal
func (b *BlockElements) UnmarshalJSON(data []byte) error {
var raw []json.RawMessage
if string(data) == "{}" {
return nil
}
err := json.Unmarshal(data, &raw)
if err != nil {
return err
}
var blockElements BlockElements
for _, r := range raw {
s := sumtype{}
err := json.Unmarshal(r, &s)
if err != nil {
return err
}
var blockElementType string
if s.TypeVal != "" {
blockElementType = s.TypeVal
}
var blockElement BlockElement
switch blockElementType {
case "image":
blockElement = &ImageBlockElement{}
case "button":
blockElement = &ButtonBlockElement{}
case "overflow":
blockElement = &OverflowBlockElement{}
case "datepicker":
blockElement = &DatePickerBlockElement{}
case "static_select":
blockElement = &SelectBlockElement{}
default:
return errors.New("unsupported block element type")
}
err = json.Unmarshal(r, blockElement)
if err != nil {
return err
}
blockElements.ElementSet = append(blockElements.ElementSet, blockElement)
}
*b = blockElements
return nil
}
// MarshalJSON implements the Marshaller interface for Accessory so that any JSON
// marshalling is delegated and proper type determination can be made before marshal
func (a *Accessory) MarshalJSON() ([]byte, error) {
bytes, err := json.Marshal(toBlockElement(a))
if err != nil {
return nil, err
}
return bytes, nil
}
// UnmarshalJSON implements the Unmarshaller interface for Accessory, so that any JSON
// unmarshalling is delegated and proper type determination can be made before unmarshal
func (a *Accessory) UnmarshalJSON(data []byte) error {
var r json.RawMessage
if string(data) == "{\"accessory\":null}" {
return nil
}
err := json.Unmarshal(data, &r)
if err != nil {
return err
}
s := sumtype{}
err = json.Unmarshal(r, &s)
if err != nil {
return err
}
var blockElementType string
if s.TypeVal != "" {
blockElementType = s.TypeVal
}
switch blockElementType {
case "image":
element, err := unmarshalBlockElement(r, &ImageBlockElement{})
if err != nil {
return err
}
a.ImageElement = element.(*ImageBlockElement)
case "button":
element, err := unmarshalBlockElement(r, &ButtonBlockElement{})
if err != nil {
return err
}
a.ButtonElement = element.(*ButtonBlockElement)
case "overflow":
element, err := unmarshalBlockElement(r, &OverflowBlockElement{})
if err != nil {
return err
}
a.OverflowElement = element.(*OverflowBlockElement)
case "datepicker":
element, err := unmarshalBlockElement(r, &DatePickerBlockElement{})
if err != nil {
return err
}
a.DatePickerElement = element.(*DatePickerBlockElement)
case "static_select":
element, err := unmarshalBlockElement(r, &SelectBlockElement{})
if err != nil {
return err
}
a.SelectElement = element.(*SelectBlockElement)
}
return nil
}
func unmarshalBlockElement(r json.RawMessage, element BlockElement) (BlockElement, error) {
err := json.Unmarshal(r, element)
if err != nil {
return nil, err
}
return element, nil
}
func toBlockElement(element *Accessory) BlockElement {
if element.ImageElement != nil {
return element.ImageElement
}
if element.ButtonElement != nil {
return element.ButtonElement
}
if element.OverflowElement != nil {
return element.OverflowElement
}
if element.DatePickerElement != nil {
return element.DatePickerElement
}
if element.SelectElement != nil {
return element.SelectElement
}
return nil
}
// MarshalJSON implements the Marshaller interface for ContextElements so that any JSON
// marshalling is delegated and proper type determination can be made before marshal
func (e *ContextElements) MarshalJSON() ([]byte, error) {
bytes, err := json.Marshal(e.Elements)
if err != nil {
return nil, err
}
return bytes, nil
}
// UnmarshalJSON implements the Unmarshaller interface for ContextElements, so that any JSON
// unmarshalling is delegated and proper type determination can be made before unmarshal
func (e *ContextElements) UnmarshalJSON(data []byte) error {
var raw []json.RawMessage
if string(data) == "{\"elements\":null}" {
return nil
}
err := json.Unmarshal(data, &raw)
if err != nil {
return err
}
for _, r := range raw {
s := sumtype{}
err := json.Unmarshal(r, &s)
if err != nil {
return err
}
var contextElementType string
if s.TypeVal != "" {
contextElementType = s.TypeVal
}
switch contextElementType {
case PlainTextType, MarkdownType:
elem, err := unmarshalBlockObject(r, &TextBlockObject{})
if err != nil {
return err
}
e.Elements = append(e.Elements, elem.(*TextBlockObject))
case "image":
elem, err := unmarshalBlockElement(r, &ImageBlockElement{})
if err != nil {
return err
}
e.Elements = append(e.Elements, elem.(*ImageBlockElement))
default:
return errors.New("unsupported context element type")
}
}
return nil
}

22
vendor/github.com/nlopes/slack/block_divider.go generated vendored Normal file
View File

@ -0,0 +1,22 @@
package slack
// DividerBlock for displaying a divider line between blocks (similar to <hr> tag in html)
//
// More Information: https://api.slack.com/reference/messaging/blocks#divider
type DividerBlock struct {
Type MessageBlockType `json:"type"`
BlockID string `json:"block_id,omitempty"`
}
// BlockType returns the type of the block
func (s DividerBlock) BlockType() MessageBlockType {
return s.Type
}
// NewDividerBlock returns a new instance of a divider block
func NewDividerBlock() *DividerBlock {
return &DividerBlock{
Type: MBTDivider,
}
}

234
vendor/github.com/nlopes/slack/block_element.go generated vendored Normal file
View File

@ -0,0 +1,234 @@
package slack
// https://api.slack.com/reference/messaging/block-elements
const (
METImage MessageElementType = "image"
METButton MessageElementType = "button"
METOverflow MessageElementType = "overflow"
METDatepicker MessageElementType = "datepicker"
MixedElementImage MixedElementType = "mixed_image"
MixedElementText MixedElementType = "mixed_text"
OptTypeStatic string = "static_select"
OptTypeExternal string = "external_select"
OptTypeUser string = "users_select"
OptTypeConversations string = "conversations_select"
OptTypeChannels string = "channels_select"
)
type MessageElementType string
type MixedElementType string
// BlockElement defines an interface that all block element types should implement.
type BlockElement interface {
ElementType() MessageElementType
}
type MixedElement interface {
MixedElementType() MixedElementType
}
type Accessory struct {
ImageElement *ImageBlockElement
ButtonElement *ButtonBlockElement
OverflowElement *OverflowBlockElement
DatePickerElement *DatePickerBlockElement
SelectElement *SelectBlockElement
}
// NewAccessory returns a new Accessory for a given block element
func NewAccessory(element BlockElement) *Accessory {
switch element.(type) {
case *ImageBlockElement:
return &Accessory{ImageElement: element.(*ImageBlockElement)}
case *ButtonBlockElement:
return &Accessory{ButtonElement: element.(*ButtonBlockElement)}
case *OverflowBlockElement:
return &Accessory{OverflowElement: element.(*OverflowBlockElement)}
case *DatePickerBlockElement:
return &Accessory{DatePickerElement: element.(*DatePickerBlockElement)}
case *SelectBlockElement:
return &Accessory{SelectElement: element.(*SelectBlockElement)}
}
return nil
}
// BlockElements is a convenience struct defined to allow dynamic unmarshalling of
// the "elements" value in Slack's JSON response, which varies depending on BlockElement type
type BlockElements struct {
ElementSet []BlockElement `json:"elements,omitempty"`
}
// ImageBlockElement An element to insert an image - this element can be used
// in section and context blocks only. If you want a block with only an image
// in it, you're looking for the image block.
//
// More Information: https://api.slack.com/reference/messaging/block-elements#image
type ImageBlockElement struct {
Type MessageElementType `json:"type"`
ImageURL string `json:"image_url"`
AltText string `json:"alt_text"`
}
// ElementType returns the type of the Element
func (s ImageBlockElement) ElementType() MessageElementType {
return s.Type
}
func (s ImageBlockElement) MixedElementType() MixedElementType {
return MixedElementImage
}
// NewImageBlockElement returns a new instance of an image block element
func NewImageBlockElement(imageURL, altText string) *ImageBlockElement {
return &ImageBlockElement{
Type: METImage,
ImageURL: imageURL,
AltText: altText,
}
}
type Style string
const (
StyleDefault Style = "default"
StylePrimary Style = "primary"
StyleDanger Style = "danger"
)
// ButtonBlockElement defines an interactive element that inserts a button. The
// button can be a trigger for anything from opening a simple link to starting
// a complex workflow.
//
// More Information: https://api.slack.com/reference/messaging/block-elements#button
type ButtonBlockElement struct {
Type MessageElementType `json:"type,omitempty"`
Text *TextBlockObject `json:"text"`
ActionID string `json:"action_id,omitempty"`
URL string `json:"url,omitempty"`
Value string `json:"value,omitempty"`
Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
Style Style `json:"style,omitempty"`
}
// ElementType returns the type of the element
func (s ButtonBlockElement) ElementType() MessageElementType {
return s.Type
}
// add styling to button object
func (s *ButtonBlockElement) WithStyle(style Style) {
s.Style = style
}
// 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{
Type: METButton,
ActionID: actionID,
Text: text,
Value: value,
}
}
// SelectBlockElement defines the simplest form of select menu, with a static list
// of options passed in when defining the element.
//
// More Information: https://api.slack.com/reference/messaging/block-elements#select
type SelectBlockElement struct {
Type string `json:"type,omitempty"`
Placeholder *TextBlockObject `json:"placeholder,omitempty"`
ActionID string `json:"action_id,omitempty"`
Options []*OptionBlockObject `json:"options,omitempty"`
OptionGroups []*OptionGroupBlockObject `json:"option_groups,omitempty"`
InitialOption *OptionBlockObject `json:"initial_option,omitempty"`
Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
}
// ElementType returns the type of the Element
func (s SelectBlockElement) ElementType() MessageElementType {
return MessageElementType(s.Type)
}
// NewOptionsSelectBlockElement returns a new instance of SelectBlockElement for use with
// the Options object only.
func NewOptionsSelectBlockElement(optType string, placeholder *TextBlockObject, actionID string, options ...*OptionBlockObject) *SelectBlockElement {
return &SelectBlockElement{
Type: optType,
Placeholder: placeholder,
ActionID: actionID,
Options: options,
}
}
// NewOptionsGroupSelectBlockElement returns a new instance of SelectBlockElement for use with
// the Options object only.
func NewOptionsGroupSelectBlockElement(
optType string,
placeholder *TextBlockObject,
actionID string,
optGroups ...*OptionGroupBlockObject,
) *SelectBlockElement {
return &SelectBlockElement{
Type: optType,
Placeholder: placeholder,
ActionID: actionID,
OptionGroups: optGroups,
}
}
// OverflowBlockElement defines the fields needed to use an overflow element.
// And Overflow Element is like a cross between a button and a select menu -
// when a user clicks on this overflow button, they will be presented with a
// list of options to choose from.
//
// More Information: https://api.slack.com/reference/messaging/block-elements#overflow
type OverflowBlockElement struct {
Type MessageElementType `json:"type"`
ActionID string `json:"action_id,omitempty"`
Options []*OptionBlockObject `json:"options"`
Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
}
// ElementType returns the type of the Element
func (s OverflowBlockElement) ElementType() MessageElementType {
return s.Type
}
// NewOverflowBlockElement returns an instance of a new Overflow Block Element
func NewOverflowBlockElement(actionID string, options ...*OptionBlockObject) *OverflowBlockElement {
return &OverflowBlockElement{
Type: METOverflow,
ActionID: actionID,
Options: options,
}
}
// DatePickerBlockElement defines an element which lets users easily select a
// date from a calendar style UI. Date picker elements can be used inside of
// section and actions blocks.
//
// More Information: https://api.slack.com/reference/messaging/block-elements#datepicker
type DatePickerBlockElement struct {
Type MessageElementType `json:"type"`
ActionID string `json:"action_id"`
Placeholder *TextBlockObject `json:"placeholder,omitempty"`
InitialDate string `json:"initial_date,omitempty"`
Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
}
// ElementType returns the type of the Element
func (s DatePickerBlockElement) ElementType() MessageElementType {
return s.Type
}
// NewDatePickerBlockElement returns an instance of a date picker element
func NewDatePickerBlockElement(actionID string) *DatePickerBlockElement {
return &DatePickerBlockElement{
Type: METDatepicker,
ActionID: actionID,
}
}

28
vendor/github.com/nlopes/slack/block_image.go generated vendored Normal file
View File

@ -0,0 +1,28 @@
package slack
// ImageBlock defines data required to display an image as a block element
//
// More Information: https://api.slack.com/reference/messaging/blocks#image
type ImageBlock struct {
Type MessageBlockType `json:"type"`
ImageURL string `json:"image_url"`
AltText string `json:"alt_text"`
BlockID string `json:"block_id,omitempty"`
Title *TextBlockObject `json:"title"`
}
// BlockType returns the type of the block
func (s ImageBlock) BlockType() MessageBlockType {
return s.Type
}
// NewImageBlock returns an instance of a new Image Block type
func NewImageBlock(imageURL, altText, blockID string, title *TextBlockObject) *ImageBlock {
return &ImageBlock{
Type: MBTImage,
ImageURL: imageURL,
AltText: altText,
BlockID: blockID,
Title: title,
}
}

215
vendor/github.com/nlopes/slack/block_object.go generated vendored Normal file
View File

@ -0,0 +1,215 @@
package slack
import (
"encoding/json"
)
// Block Objects are also known as Composition Objects
//
// For more information: https://api.slack.com/reference/messaging/composition-objects
// BlockObject defines an interface that all block object types should
// implement.
// @TODO: Is this interface needed?
// blockObject object types
const (
MarkdownType = "mrkdwn"
PlainTextType = "plain_text"
// The following objects don't actually have types and their corresponding
// const values are just for internal use
motConfirmation = "confirm"
motOption = "option"
motOptionGroup = "option_group"
)
type MessageObjectType string
type blockObject interface {
validateType() MessageObjectType
}
type BlockObjects struct {
TextObjects []*TextBlockObject
ConfirmationObjects []*ConfirmationBlockObject
OptionObjects []*OptionBlockObject
OptionGroupObjects []*OptionGroupBlockObject
}
// UnmarshalJSON implements the Unmarshaller interface for BlockObjects, so that any JSON
// unmarshalling is delegated and proper type determination can be made before unmarshal
func (b *BlockObjects) UnmarshalJSON(data []byte) error {
var raw []json.RawMessage
err := json.Unmarshal(data, &raw)
if err != nil {
return err
}
for _, r := range raw {
var obj map[string]interface{}
err := json.Unmarshal(r, &obj)
if err != nil {
return err
}
blockObjectType := getBlockObjectType(obj)
switch blockObjectType {
case PlainTextType, MarkdownType:
object, err := unmarshalBlockObject(r, &TextBlockObject{})
if err != nil {
return err
}
b.TextObjects = append(b.TextObjects, object.(*TextBlockObject))
case motConfirmation:
object, err := unmarshalBlockObject(r, &ConfirmationBlockObject{})
if err != nil {
return err
}
b.ConfirmationObjects = append(b.ConfirmationObjects, object.(*ConfirmationBlockObject))
case motOption:
object, err := unmarshalBlockObject(r, &OptionBlockObject{})
if err != nil {
return err
}
b.OptionObjects = append(b.OptionObjects, object.(*OptionBlockObject))
case motOptionGroup:
object, err := unmarshalBlockObject(r, &OptionGroupBlockObject{})
if err != nil {
return err
}
b.OptionGroupObjects = append(b.OptionGroupObjects, object.(*OptionGroupBlockObject))
}
}
return nil
}
// Ideally would have a better way to identify the block objects for
// type casting at time of unmarshalling, should be adapted if possible
// to accomplish in a more reliable manner.
func getBlockObjectType(obj map[string]interface{}) string {
if t, ok := obj["type"].(string); ok {
return t
}
if _, ok := obj["confirm"].(string); ok {
return "confirm"
}
if _, ok := obj["options"].(string); ok {
return "option_group"
}
if _, ok := obj["text"].(string); ok {
if _, ok := obj["value"].(string); ok {
return "option"
}
}
return ""
}
func unmarshalBlockObject(r json.RawMessage, object blockObject) (blockObject, error) {
err := json.Unmarshal(r, object)
if err != nil {
return nil, err
}
return object, nil
}
// TextBlockObject defines a text element object to be used with blocks
//
// More Information: https://api.slack.com/reference/messaging/composition-objects#text
type TextBlockObject struct {
Type string `json:"type"`
Text string `json:"text"`
Emoji bool `json:"emoji,omitempty"`
Verbatim bool `json:"verbatim,omitempty"`
}
// validateType enforces block objects for element and block parameters
func (s TextBlockObject) validateType() MessageObjectType {
return MessageObjectType(s.Type)
}
// validateType enforces block objects for element and block parameters
func (s TextBlockObject) MixedElementType() MixedElementType {
return MixedElementText
}
// NewTextBlockObject returns an instance of a new Text Block Object
func NewTextBlockObject(elementType, text string, emoji, verbatim bool) *TextBlockObject {
return &TextBlockObject{
Type: elementType,
Text: text,
Emoji: emoji,
Verbatim: verbatim,
}
}
// ConfirmationBlockObject defines a dialog that provides a confirmation step to
// any interactive element. This dialog will ask the user to confirm their action by
// offering a confirm and deny buttons.
//
// More Information: https://api.slack.com/reference/messaging/composition-objects#confirm
type ConfirmationBlockObject struct {
Title *TextBlockObject `json:"title"`
Text *TextBlockObject `json:"text"`
Confirm *TextBlockObject `json:"confirm"`
Deny *TextBlockObject `json:"deny"`
}
// validateType enforces block objects for element and block parameters
func (s ConfirmationBlockObject) validateType() MessageObjectType {
return motConfirmation
}
// NewConfirmationBlockObject returns an instance of a new Confirmation Block Object
func NewConfirmationBlockObject(title, text, confirm, deny *TextBlockObject) *ConfirmationBlockObject {
return &ConfirmationBlockObject{
Title: title,
Text: text,
Confirm: confirm,
Deny: deny,
}
}
// OptionBlockObject represents a single selectable item in a select menu
//
// More Information: https://api.slack.com/reference/messaging/composition-objects#option
type OptionBlockObject struct {
Text *TextBlockObject `json:"text"`
Value string `json:"value"`
}
// NewOptionBlockObject returns an instance of a new Option Block Element
func NewOptionBlockObject(value string, text *TextBlockObject) *OptionBlockObject {
return &OptionBlockObject{
Text: text,
Value: value,
}
}
// validateType enforces block objects for element and block parameters
func (s OptionBlockObject) validateType() MessageObjectType {
return motOption
}
// OptionGroupBlockObject Provides a way to group options in a select menu.
//
// More Information: https://api.slack.com/reference/messaging/composition-objects#option-group
type OptionGroupBlockObject struct {
Label *TextBlockObject `json:"label"`
Options []*OptionBlockObject `json:"options"`
}
// validateType enforces block objects for element and block parameters
func (s OptionGroupBlockObject) validateType() MessageObjectType {
return motOptionGroup
}
// NewOptionGroupBlockElement returns an instance of a new option group block element
func NewOptionGroupBlockElement(label *TextBlockObject, options ...*OptionBlockObject) *OptionGroupBlockObject {
return &OptionGroupBlockObject{
Label: label,
Options: options,
}
}

27
vendor/github.com/nlopes/slack/block_section.go generated vendored Normal file
View File

@ -0,0 +1,27 @@
package slack
// SectionBlock defines a new block of type section
//
// More Information: https://api.slack.com/reference/messaging/blocks#section
type SectionBlock struct {
Type MessageBlockType `json:"type"`
Text *TextBlockObject `json:"text,omitempty"`
BlockID string `json:"block_id,omitempty"`
Fields []*TextBlockObject `json:"fields,omitempty"`
Accessory *Accessory `json:"accessory,omitempty"`
}
// BlockType returns the type of the block
func (s SectionBlock) BlockType() MessageBlockType {
return s.Type
}
// NewSectionBlock returns a new instance of a section block to be rendered
func NewSectionBlock(textObj *TextBlockObject, fields []*TextBlockObject, accessory *Accessory) *SectionBlock {
return &SectionBlock{
Type: MBTSection,
Text: textObj,
Fields: fields,
Accessory: accessory,
}
}

View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
)
@ -19,15 +18,17 @@ type botResponseFull struct {
SlackResponse
}
func botRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*botResponseFull, error) {
func (api *Client) botRequest(ctx context.Context, path string, values url.Values) (*botResponseFull, error) {
response := &botResponseFull{}
err := postSlackMethod(ctx, client, path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
if err := response.Err(); err != nil {
return nil, err
}
return response, nil
}
@ -43,7 +44,7 @@ func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, err
"bot": {bot},
}
response, err := botRequest(ctx, api.httpclient, "bots.info", values, api.debug)
response, err := api.botRequest(ctx, "bots.info", values)
if err != nil {
return nil, err
}

View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
@ -19,25 +18,50 @@ type channelResponseFull struct {
// Channel contains information about the channel
type Channel struct {
groupConversation
GroupConversation
IsChannel bool `json:"is_channel"`
IsGeneral bool `json:"is_general"`
IsMember bool `json:"is_member"`
Locale string `json:"locale"`
}
func channelRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*channelResponseFull, error) {
func (api *Client) channelRequest(ctx context.Context, path string, values url.Values) (*channelResponseFull, error) {
response := &channelResponseFull{}
err := postForm(ctx, client, SLACK_API+path, values, response, debug)
err := postForm(ctx, api.httpclient, api.endpoint+path, values, response, api)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
if err := response.Err(); err != nil {
return nil, err
}
return response, nil
}
type channelsConfig struct {
values url.Values
}
// GetChannelsOption option provided when getting channels.
type GetChannelsOption func(*channelsConfig) error
// GetChannelsOptionExcludeMembers excludes the members collection from each channel.
func GetChannelsOptionExcludeMembers() GetChannelsOption {
return func(config *channelsConfig) error {
config.values.Add("exclude_members", "true")
return nil
}
}
// GetChannelsOptionExcludeArchived excludes archived channels from results.
func GetChannelsOptionExcludeArchived() GetChannelsOption {
return func(config *channelsConfig) error {
config.values.Add("exclude_archived", "true")
return nil
}
}
// ArchiveChannel archives the given channel
// see https://api.slack.com/methods/channels.archive
func (api *Client) ArchiveChannel(channelID string) error {
@ -52,7 +76,7 @@ func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string)
"channel": {channelID},
}
_, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug)
_, err = api.channelRequest(ctx, "channels.archive", values)
return err
}
@ -70,7 +94,7 @@ func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string
"channel": {channelID},
}
_, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug)
_, err = api.channelRequest(ctx, "channels.unarchive", values)
return err
}
@ -88,7 +112,7 @@ func (api *Client) CreateChannelContext(ctx context.Context, channelName string)
"name": {channelName},
}
response, err := channelRequest(ctx, api.httpclient, "channels.create", values, api.debug)
response, err := api.channelRequest(ctx, "channels.create", values)
if err != nil {
return nil, err
}
@ -133,7 +157,7 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID strin
}
}
response, err := channelRequest(ctx, api.httpclient, "channels.history", values, api.debug)
response, err := api.channelRequest(ctx, "channels.history", values)
if err != nil {
return nil, err
}
@ -150,11 +174,12 @@ func (api *Client) GetChannelInfo(channelID string) (*Channel, error) {
// see https://api.slack.com/methods/channels.info
func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) (*Channel, error) {
values := url.Values{
"token": {api.token},
"channel": {channelID},
"token": {api.token},
"channel": {channelID},
"include_locale": {strconv.FormatBool(true)},
}
response, err := channelRequest(ctx, api.httpclient, "channels.info", values, api.debug)
response, err := api.channelRequest(ctx, "channels.info", values)
if err != nil {
return nil, err
}
@ -167,7 +192,7 @@ func (api *Client) InviteUserToChannel(channelID, user string) (*Channel, error)
return api.InviteUserToChannelContext(context.Background(), channelID, user)
}
// InviteUserToChannelCustom invites a user to a given channel and returns a *Channel with a custom context
// InviteUserToChannelContext invites a user to a given channel and returns a *Channel with a custom context
// see https://api.slack.com/methods/channels.invite
func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) {
values := url.Values{
@ -176,7 +201,7 @@ func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, us
"user": {user},
}
response, err := channelRequest(ctx, api.httpclient, "channels.invite", values, api.debug)
response, err := api.channelRequest(ctx, "channels.invite", values)
if err != nil {
return nil, err
}
@ -197,7 +222,7 @@ func (api *Client) JoinChannelContext(ctx context.Context, channelName string) (
"name": {channelName},
}
response, err := channelRequest(ctx, api.httpclient, "channels.join", values, api.debug)
response, err := api.channelRequest(ctx, "channels.join", values)
if err != nil {
return nil, err
}
@ -218,7 +243,7 @@ func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (b
"channel": {channelID},
}
response, err := channelRequest(ctx, api.httpclient, "channels.leave", values, api.debug)
response, err := api.channelRequest(ctx, "channels.leave", values)
if err != nil {
return false, err
}
@ -241,27 +266,35 @@ func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, us
"user": {user},
}
_, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug)
_, err = api.channelRequest(ctx, "channels.kick", values)
return err
}
// GetChannels retrieves all the channels
// see https://api.slack.com/methods/channels.list
func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) {
return api.GetChannelsContext(context.Background(), excludeArchived)
func (api *Client) GetChannels(excludeArchived bool, options ...GetChannelsOption) ([]Channel, error) {
return api.GetChannelsContext(context.Background(), excludeArchived, options...)
}
// GetChannelsContext retrieves all the channels with a custom context
// see https://api.slack.com/methods/channels.list
func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool) ([]Channel, error) {
values := url.Values{
"token": {api.token},
func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool, options ...GetChannelsOption) ([]Channel, error) {
config := channelsConfig{
values: url.Values{
"token": {api.token},
},
}
if excludeArchived {
values.Add("exclude_archived", "1")
options = append(options, GetChannelsOptionExcludeArchived())
}
response, err := channelRequest(ctx, api.httpclient, "channels.list", values, api.debug)
for _, opt := range options {
if err := opt(&config); err != nil {
return nil, err
}
}
response, err := api.channelRequest(ctx, "channels.list", config.values)
if err != nil {
return nil, err
}
@ -288,7 +321,7 @@ func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts
"ts": {ts},
}
_, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug)
_, err = api.channelRequest(ctx, "channels.mark", values)
return err
}
@ -309,7 +342,7 @@ func (api *Client) RenameChannelContext(ctx context.Context, channelID, name str
// XXX: the created entry in this call returns a string instead of a number
// so I may have to do some workaround to solve it.
response, err := channelRequest(ctx, api.httpclient, "channels.rename", values, api.debug)
response, err := api.channelRequest(ctx, "channels.rename", values)
if err != nil {
return nil, err
}
@ -331,7 +364,7 @@ func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purp
"purpose": {purpose},
}
response, err := channelRequest(ctx, api.httpclient, "channels.setPurpose", values, api.debug)
response, err := api.channelRequest(ctx, "channels.setPurpose", values)
if err != nil {
return "", err
}
@ -353,7 +386,7 @@ func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic
"topic": {topic},
}
response, err := channelRequest(ctx, api.httpclient, "channels.setTopic", values, api.debug)
response, err := api.channelRequest(ctx, "channels.setTopic", values)
if err != nil {
return "", err
}
@ -374,7 +407,7 @@ func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thre
"channel": {channelID},
"thread_ts": {thread_ts},
}
response, err := channelRequest(ctx, api.httpclient, "channels.replies", values, api.debug)
response, err := api.channelRequest(ctx, "channels.replies", values)
if err != nil {
return nil, err
}

View File

@ -4,7 +4,8 @@ import (
"context"
"encoding/json"
"net/url"
"strings"
"github.com/nlopes/slack/slackutilsx"
)
const (
@ -42,19 +43,18 @@ func (c chatResponseFull) getMessageTimestamp() string {
// PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request
type PostMessageParameters struct {
Username string `json:"username"`
AsUser bool `json:"as_user"`
Parse string `json:"parse"`
ThreadTimestamp string `json:"thread_ts"`
ReplyBroadcast bool `json:"reply_broadcast"`
LinkNames int `json:"link_names"`
Attachments []Attachment `json:"attachments"`
UnfurlLinks bool `json:"unfurl_links"`
UnfurlMedia bool `json:"unfurl_media"`
IconURL string `json:"icon_url"`
IconEmoji string `json:"icon_emoji"`
Markdown bool `json:"mrkdwn,omitempty"`
EscapeText bool `json:"escape_text"`
Username string `json:"username"`
AsUser bool `json:"as_user"`
Parse string `json:"parse"`
ThreadTimestamp string `json:"thread_ts"`
ReplyBroadcast bool `json:"reply_broadcast"`
LinkNames int `json:"link_names"`
UnfurlLinks bool `json:"unfurl_links"`
UnfurlMedia bool `json:"unfurl_media"`
IconURL string `json:"icon_url"`
IconEmoji string `json:"icon_emoji"`
Markdown bool `json:"mrkdwn,omitempty"`
EscapeText bool `json:"escape_text"`
// chat.postEphemeral support
Channel string `json:"channel"`
@ -70,7 +70,6 @@ func NewPostMessageParameters() PostMessageParameters {
Parse: DEFAULT_MESSAGE_PARSE,
ThreadTimestamp: DEFAULT_MESSAGE_THREAD_TIMESTAMP,
LinkNames: DEFAULT_MESSAGE_LINK_NAMES,
Attachments: nil,
UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS,
UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA,
IconURL: DEFAULT_MESSAGE_ICON_URL,
@ -95,26 +94,24 @@ func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTim
// PostMessage sends a message to a channel.
// 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(channel, text string, params PostMessageParameters) (string, string, error) {
func (api *Client) PostMessage(channelID string, options ...MsgOption) (string, string, error) {
respChannel, respTimestamp, _, err := api.SendMessageContext(
context.Background(),
channel,
MsgOptionText(text, params.EscapeText),
MsgOptionAttachments(params.Attachments...),
MsgOptionPostMessageParameters(params),
channelID,
MsgOptionPost(),
MsgOptionCompose(options...),
)
return respChannel, respTimestamp, err
}
// PostMessageContext sends a message to a channel with a custom context
// For more details, see PostMessage documentation
func (api *Client) PostMessageContext(ctx context.Context, channel, text string, params PostMessageParameters) (string, string, error) {
// For more details, see PostMessage documentation.
func (api *Client) PostMessageContext(ctx context.Context, channelID string, options ...MsgOption) (string, string, error) {
respChannel, respTimestamp, _, err := api.SendMessageContext(
ctx,
channel,
MsgOptionText(text, params.EscapeText),
MsgOptionAttachments(params.Attachments...),
MsgOptionPostMessageParameters(params),
channelID,
MsgOptionPost(),
MsgOptionCompose(options...),
)
return respChannel, respTimestamp, err
}
@ -134,18 +131,23 @@ func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption)
// PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context
// For more details, see PostEphemeral documentation
func (api *Client) PostEphemeralContext(ctx context.Context, channelID, userID string, options ...MsgOption) (timestamp string, err error) {
_, timestamp, _, err = api.SendMessageContext(ctx, channelID, append(options, MsgOptionPostEphemeral2(userID))...)
_, timestamp, _, err = api.SendMessageContext(ctx, channelID, MsgOptionPostEphemeral(userID), MsgOptionCompose(options...))
return timestamp, err
}
// UpdateMessage updates a message in a channel
func (api *Client) UpdateMessage(channelID, timestamp, text string) (string, string, string, error) {
return api.UpdateMessageContext(context.Background(), channelID, timestamp, text)
func (api *Client) UpdateMessage(channelID, timestamp string, options ...MsgOption) (string, string, string, error) {
return api.SendMessageContext(context.Background(), channelID, MsgOptionUpdate(timestamp), MsgOptionCompose(options...))
}
// UpdateMessageContext updates a message in a channel
func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp, text string) (string, string, string, error) {
return api.SendMessageContext(ctx, channelID, MsgOptionUpdate(timestamp), MsgOptionText(text, true))
func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp string, options ...MsgOption) (string, string, string, error) {
return api.SendMessageContext(ctx, channelID, MsgOptionUpdate(timestamp), MsgOptionCompose(options...))
}
// UnfurlMessage unfurls a message in a channel
func (api *Client) UnfurlMessage(channelID, timestamp string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) {
return api.SendMessageContext(context.Background(), channelID, MsgOptionUnfurl(timestamp, unfurls), MsgOptionCompose(options...))
}
// SendMessage more flexible method for configuring messages.
@ -160,26 +162,29 @@ func (api *Client) SendMessageContext(ctx context.Context, channelID string, opt
response chatResponseFull
)
if config, err = applyMsgOptions(api.token, channelID, options...); err != nil {
if config, err = applyMsgOptions(api.token, channelID, api.endpoint, options...); err != nil {
return "", "", "", err
}
if err = postSlackMethod(ctx, api.httpclient, string(config.mode), config.values, &response, api.debug); err != nil {
if err = postForm(ctx, api.httpclient, config.endpoint, config.values, &response, api); err != nil {
return "", "", "", err
}
return response.Channel, response.getMessageTimestamp(), response.Text, response.Err()
}
// ApplyMsgOptions utility function for debugging/testing chat requests.
func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) {
config, err := applyMsgOptions(token, channel, options...)
return string(config.mode), config.values, err
// UnsafeApplyMsgOptions utility function for debugging/testing chat requests.
// NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this function
// will be supported by the library.
func UnsafeApplyMsgOptions(token, channel, apiurl string, options ...MsgOption) (string, url.Values, error) {
config, err := applyMsgOptions(token, channel, apiurl, options...)
return config.endpoint, config.values, err
}
func applyMsgOptions(token, channel string, options ...MsgOption) (sendConfig, error) {
func applyMsgOptions(token, channel, apiurl string, options ...MsgOption) (sendConfig, error) {
config := sendConfig{
mode: chatPostMessage,
apiurl: apiurl,
endpoint: apiurl + string(chatPostMessage),
values: url.Values{
"token": {token},
"channel": {channel},
@ -195,11 +200,6 @@ func applyMsgOptions(token, channel string, options ...MsgOption) (sendConfig, e
return config, nil
}
func escapeMessage(message string) string {
replacer := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
return replacer.Replace(message)
}
type sendMode string
const (
@ -208,11 +208,13 @@ const (
chatDelete sendMode = "chat.delete"
chatPostEphemeral sendMode = "chat.postEphemeral"
chatMeMessage sendMode = "chat.meMessage"
chatUnfurl sendMode = "chat.unfurl"
)
type sendConfig struct {
mode sendMode
values url.Values
apiurl string
endpoint string
values url.Values
}
// MsgOption option provided when sending a message.
@ -221,26 +223,16 @@ type MsgOption func(*sendConfig) error
// MsgOptionPost posts a messages, this is the default.
func MsgOptionPost() MsgOption {
return func(config *sendConfig) error {
config.mode = chatPostMessage
config.endpoint = config.apiurl + string(chatPostMessage)
config.values.Del("ts")
return nil
}
}
// MsgOptionPostEphemeral - DEPRECATED: use MsgOptionPostEphemeral2
// posts an ephemeral message.
func MsgOptionPostEphemeral() MsgOption {
// MsgOptionPostEphemeral - posts an ephemeral message to the provided user.
func MsgOptionPostEphemeral(userID string) MsgOption {
return func(config *sendConfig) error {
config.mode = chatPostEphemeral
config.values.Del("ts")
return nil
}
}
// MsgOptionPostEphemeral2 - posts an ephemeral message to the provided user.
func MsgOptionPostEphemeral2(userID string) MsgOption {
return func(config *sendConfig) error {
config.mode = chatPostEphemeral
config.endpoint = config.apiurl + string(chatPostEphemeral)
MsgOptionUser(userID)(config)
config.values.Del("ts")
@ -251,7 +243,7 @@ func MsgOptionPostEphemeral2(userID string) MsgOption {
// MsgOptionMeMessage posts a "me message" type from the calling user
func MsgOptionMeMessage() MsgOption {
return func(config *sendConfig) error {
config.mode = chatMeMessage
config.endpoint = config.apiurl + string(chatMeMessage)
return nil
}
}
@ -259,7 +251,7 @@ func MsgOptionMeMessage() MsgOption {
// MsgOptionUpdate updates a message based on the timestamp.
func MsgOptionUpdate(timestamp string) MsgOption {
return func(config *sendConfig) error {
config.mode = chatUpdate
config.endpoint = config.apiurl + string(chatUpdate)
config.values.Add("ts", timestamp)
return nil
}
@ -268,12 +260,25 @@ func MsgOptionUpdate(timestamp string) MsgOption {
// MsgOptionDelete deletes a message based on the timestamp.
func MsgOptionDelete(timestamp string) MsgOption {
return func(config *sendConfig) error {
config.mode = chatDelete
config.endpoint = config.apiurl + string(chatDelete)
config.values.Add("ts", timestamp)
return nil
}
}
// MsgOptionUnfurl unfurls a message based on the timestamp.
func MsgOptionUnfurl(timestamp string, unfurls map[string]Attachment) MsgOption {
return func(config *sendConfig) error {
config.endpoint = config.apiurl + string(chatUnfurl)
config.values.Add("ts", timestamp)
unfurlsStr, err := json.Marshal(unfurls)
if err == nil {
config.values.Add("unfurls", string(unfurlsStr))
}
return err
}
}
// MsgOptionAsUser whether or not to send the message as the user.
func MsgOptionAsUser(b bool) MsgOption {
return func(config *sendConfig) error {
@ -292,12 +297,20 @@ func MsgOptionUser(userID string) MsgOption {
}
}
// MsgOptionUsername set the username for the message.
func MsgOptionUsername(username string) MsgOption {
return func(config *sendConfig) error {
config.values.Set("username", username)
return nil
}
}
// MsgOptionText provide the text for the message, optionally escape the provided
// text.
func MsgOptionText(text string, escape bool) MsgOption {
return func(config *sendConfig) error {
if escape {
text = escapeMessage(text)
text = slackutilsx.EscapeMessage(text)
}
config.values.Add("text", text)
return nil
@ -319,6 +332,21 @@ func MsgOptionAttachments(attachments ...Attachment) MsgOption {
}
}
// MsgOptionBlocks sets blocks for the message
func MsgOptionBlocks(blocks ...Block) MsgOption {
return func(config *sendConfig) error {
if blocks == nil {
return nil
}
blocks, err := json.Marshal(blocks)
if err == nil {
config.values.Set("blocks", string(blocks))
}
return err
}
}
// MsgOptionEnableLinkUnfurl enables link unfurling
func MsgOptionEnableLinkUnfurl() MsgOption {
return func(config *sendConfig) error {
@ -367,7 +395,7 @@ func MsgOptionBroadcast() MsgOption {
}
}
// this function combines multiple options into a single option.
// MsgOptionCompose combines multiple options into a single option.
func MsgOptionCompose(options ...MsgOption) MsgOption {
return func(c *sendConfig) error {
for _, opt := range options {
@ -379,19 +407,48 @@ func MsgOptionCompose(options ...MsgOption) MsgOption {
}
}
// MsgOptionParse set parse option.
func MsgOptionParse(b bool) MsgOption {
return func(c *sendConfig) error {
var v string
if b {
v = "1"
v = "full"
} else {
v = "0"
v = "none"
}
c.values.Set("parse", v)
return nil
}
}
// MsgOptionIconURL sets an icon URL
func MsgOptionIconURL(iconURL string) MsgOption {
return func(c *sendConfig) error {
c.values.Set("icon_url", iconURL)
return nil
}
}
// MsgOptionIconEmoji sets an icon emoji
func MsgOptionIconEmoji(iconEmoji string) MsgOption {
return func(c *sendConfig) error {
c.values.Set("icon_emoji", iconEmoji)
return nil
}
}
// UnsafeMsgOptionEndpoint deliver the message to the specified endpoint.
// NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this Option
// will be supported by the library, it is subject to change without notice that
// may result in compilation errors or runtime behaviour changes.
func UnsafeMsgOptionEndpoint(endpoint string, update func(url.Values)) MsgOption {
return func(config *sendConfig) error {
config.endpoint = endpoint
update(config.values)
return nil
}
}
// MsgOptionPostMessageParameters maintain backwards compatibility.
func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
return func(config *sendConfig) error {
@ -446,3 +503,38 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
return nil
}
}
// PermalinkParameters are the parameters required to get a permalink to a
// message. Slack documentation can be found here:
// https://api.slack.com/methods/chat.getPermalink
type PermalinkParameters struct {
Channel string
Ts string
}
// GetPermalink returns the permalink for a message. It takes
// PermalinkParameters and returns a string containing the permalink. It
// returns an error if unable to retrieve the permalink.
func (api *Client) GetPermalink(params *PermalinkParameters) (string, error) {
return api.GetPermalinkContext(context.Background(), params)
}
// GetPermalinkContext returns the permalink for a message using a custom context.
func (api *Client) GetPermalinkContext(ctx context.Context, params *PermalinkParameters) (string, error) {
values := url.Values{
"token": {api.token},
"channel": {params.Channel},
"message_ts": {params.Ts},
}
response := struct {
Channel string `json:"channel"`
Permalink string `json:"permalink"`
SlackResponse
}{}
err := api.getMethod(ctx, "chat.getPermalink", values, &response)
if err != nil {
return "", err
}
return response.Permalink, response.Err()
}

View File

@ -2,14 +2,13 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
"strings"
)
// Conversation is the foundation for IM and BaseGroupConversation
type conversation struct {
type Conversation struct {
ID string `json:"id"`
Created JSONTime `json:"created"`
IsOpen bool `json:"is_open"`
@ -36,8 +35,8 @@ type conversation struct {
}
// GroupConversation is the foundation for Group and Channel
type groupConversation struct {
conversation
type GroupConversation struct {
Conversation
Name string `json:"name"`
Creator string `json:"creator"`
IsArchived bool `json:"is_archived"`
@ -66,6 +65,14 @@ type GetUsersInConversationParameters struct {
Limit int
}
type GetConversationsForUserParameters struct {
UserID string
Cursor string
Types []string
Limit int
ExcludeArchived bool
}
type responseMetaData struct {
NextCursor string `json:"next_cursor"`
}
@ -92,16 +99,56 @@ func (api *Client) GetUsersInConversationContext(ctx context.Context, params *Ge
ResponseMetaData responseMetaData `json:"response_metadata"`
SlackResponse
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.members", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.members", values, &response)
if err != nil {
return nil, "", err
}
if !response.Ok {
return nil, "", errors.New(response.Error)
if err := response.Err(); err != nil {
return nil, "", err
}
return response.Members, response.ResponseMetaData.NextCursor, nil
}
// GetConversationsForUser returns the list conversations for a given user
func (api *Client) GetConversationsForUser(params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) {
return api.GetConversationsForUserContext(context.Background(), params)
}
// GetConversationsForUserContext returns the list conversations for a given user with a custom context
func (api *Client) GetConversationsForUserContext(ctx context.Context, params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) {
values := url.Values{
"token": {api.token},
}
if params.UserID != "" {
values.Add("user", params.UserID)
}
if params.Cursor != "" {
values.Add("cursor", params.Cursor)
}
if params.Limit != 0 {
values.Add("limit", strconv.Itoa(params.Limit))
}
if params.Types != nil {
values.Add("types", strings.Join(params.Types, ","))
}
if params.ExcludeArchived {
values.Add("exclude_archived", "true")
}
response := struct {
Channels []Channel `json:"channels"`
ResponseMetaData responseMetaData `json:"response_metadata"`
SlackResponse
}{}
err = api.postMethod(ctx, "users.conversations", values, &response)
if err != nil {
return nil, "", err
}
return response.Channels, response.ResponseMetaData.NextCursor, response.Err()
}
// ArchiveConversation archives a conversation
func (api *Client) ArchiveConversation(channelID string) error {
return api.ArchiveConversationContext(context.Background(), channelID)
@ -114,7 +161,7 @@ func (api *Client) ArchiveConversationContext(ctx context.Context, channelID str
"channel": {channelID},
}
response := SlackResponse{}
err := postSlackMethod(ctx, api.httpclient, "conversations.archive", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.archive", values, &response)
if err != nil {
return err
}
@ -134,7 +181,7 @@ func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID s
"channel": {channelID},
}
response := SlackResponse{}
err := postSlackMethod(ctx, api.httpclient, "conversations.unarchive", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.unarchive", values, &response)
if err != nil {
return err
}
@ -158,7 +205,7 @@ func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID,
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.setTopic", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.setTopic", values, &response)
if err != nil {
return nil, err
}
@ -182,7 +229,7 @@ func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelI
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.setPurpose", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.setPurpose", values, &response)
if err != nil {
return nil, err
}
@ -206,7 +253,7 @@ func (api *Client) RenameConversationContext(ctx context.Context, channelID, cha
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.rename", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.rename", values, &response)
if err != nil {
return nil, err
}
@ -230,7 +277,7 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.invite", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.invite", values, &response)
if err != nil {
return nil, err
}
@ -251,7 +298,7 @@ func (api *Client) KickUserFromConversationContext(ctx context.Context, channelI
"user": {user},
}
response := SlackResponse{}
err := postSlackMethod(ctx, api.httpclient, "conversations.kick", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.kick", values, &response)
if err != nil {
return err
}
@ -276,7 +323,7 @@ func (api *Client) CloseConversationContext(ctx context.Context, channelID strin
AlreadyClosed bool `json:"already_closed"`
}{}
err = postSlackMethod(ctx, api.httpclient, "conversations.close", values, &response, api.debug)
err = api.postMethod(ctx, "conversations.close", values, &response)
if err != nil {
return false, false, err
}
@ -296,13 +343,12 @@ func (api *Client) CreateConversationContext(ctx context.Context, channelName st
"name": {channelName},
"is_private": {strconv.FormatBool(isPrivate)},
}
response, err := channelRequest(
ctx, api.httpclient, "conversations.create", values, api.debug)
response, err := api.channelRequest(ctx, "conversations.create", values)
if err != nil {
return nil, err
}
return &response.Channel, response.Err()
return &response.Channel, nil
}
// GetConversationInfo retrieves information about a conversation
@ -317,8 +363,7 @@ func (api *Client) GetConversationInfoContext(ctx context.Context, channelID str
"channel": {channelID},
"include_locale": {strconv.FormatBool(includeLocale)},
}
response, err := channelRequest(
ctx, api.httpclient, "conversations.info", values, api.debug)
response, err := api.channelRequest(ctx, "conversations.info", values)
if err != nil {
return nil, err
}
@ -338,7 +383,7 @@ func (api *Client) LeaveConversationContext(ctx context.Context, channelID strin
"channel": {channelID},
}
response, err := channelRequest(ctx, api.httpclient, "conversations.leave", values, api.debug)
response, err := api.channelRequest(ctx, "conversations.leave", values)
if err != nil {
return false, err
}
@ -394,7 +439,7 @@ func (api *Client) GetConversationRepliesContext(ctx context.Context, params *Ge
Messages []Message `json:"messages"`
}{}
err = postSlackMethod(ctx, api.httpclient, "conversations.replies", values, &response, api.debug)
err = api.postMethod(ctx, "conversations.replies", values, &response)
if err != nil {
return nil, false, "", err
}
@ -434,7 +479,7 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve
ResponseMetaData responseMetaData `json:"response_metadata"`
SlackResponse
}{}
err = postSlackMethod(ctx, api.httpclient, "conversations.list", values, &response, api.debug)
err = api.postMethod(ctx, "conversations.list", values, &response)
if err != nil {
return nil, "", err
}
@ -471,7 +516,7 @@ func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConv
AlreadyOpen bool `json:"already_open"`
SlackResponse
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.open", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.open", values, &response)
if err != nil {
return nil, false, false, err
}
@ -495,7 +540,7 @@ func (api *Client) JoinConversationContext(ctx context.Context, channelID string
} `json:"response_metadata"`
SlackResponse
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.join", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.join", values, &response)
if err != nil {
return nil, "", nil, err
}
@ -557,12 +602,10 @@ func (api *Client) GetConversationHistoryContext(ctx context.Context, params *Ge
response := GetConversationHistoryResponse{}
err := postSlackMethod(ctx, api.httpclient, "conversations.history", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.history", values, &response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return &response, nil
return &response, response.Err()
}

View File

@ -3,105 +3,116 @@ package slack
import (
"context"
"encoding/json"
"errors"
"strings"
)
// InputType is the type of the dialog input type
type InputType string
const (
// InputTypeText textfield input
InputTypeText InputType = "text"
// InputTypeTextArea textarea input
InputTypeTextArea InputType = "textarea"
// InputTypeSelect select menus input
InputTypeSelect InputType = "select"
)
// DialogInput for dialogs input type text or menu
type DialogInput struct {
Type InputType `json:"type"`
Label string `json:"label"`
Name string `json:"name"`
Placeholder string `json:"placeholder"`
Optional bool `json:"optional"`
Hint string `json:"hint"`
}
// DialogTrigger ...
type DialogTrigger struct {
TriggerId string `json:"trigger_id"` //Required. Must respond within 3 seconds.
TriggerID string `json:"trigger_id"` //Required. Must respond within 3 seconds.
Dialog Dialog `json:"dialog"` //Required.
}
// Dialog as in Slack dialogs
// https://api.slack.com/dialogs#option_element_attributes#top-level_dialog_attributes
type Dialog struct {
CallbackId string `json:"callback_id"` //Required.
Title string `json:"title"` //Required.
SubmitLabel string `json:"submit_label,omitempty"` //Optional. Default value is 'Submit'
NotifyOnCancel bool `json:"notify_on_cancel,omitempty"` //Optional. Default value is false
Elements []DialogElement `json:"elements"` //Required.
TriggerID string `json:"trigger_id"` // Required
CallbackID string `json:"callback_id"` // Required
State string `json:"state,omitempty"` // Optional
Title string `json:"title"`
SubmitLabel string `json:"submit_label,omitempty"`
NotifyOnCancel bool `json:"notify_on_cancel"`
Elements []DialogElement `json:"elements"`
}
// DialogElement abstract type for dialogs.
type DialogElement interface{}
type DialogTextElement struct {
Label string `json:"label"` //Required.
Name string `json:"name"` //Required.
Type string `json:"type"` //Required. Allowed values: "text", "textarea", "select".
Placeholder string `json:"placeholder,omitempty"` //Optional.
Optional bool `json:"optional,omitempty"` //Optional. Default value is false
Value string `json:"value,omitempty"` //Optional.
MaxLength int `json:"max_length,omitempty"` //Optional.
MinLength int `json:"min_length,omitempty"` //Optional,. Default value is 0
Hint string `json:"hint,omitempty"` //Optional.
Subtype string `json:"subtype,omitempty"` //Optional. Allowed values: "email", "number", "tel", "url".
// DialogCallback DEPRECATED use InteractionCallback
type DialogCallback InteractionCallback
// DialogSubmissionCallback is sent from Slack when a user submits a form from within a dialog
type DialogSubmissionCallback struct {
State string `json:"state,omitempty"`
Submission map[string]string `json:"submission"`
}
type DialogSelectElement struct {
Label string `json:"label"` //Required.
Name string `json:"name"` //Required.
Type string `json:"type"` //Required. Allowed values: "text", "textarea", "select".
Placeholder string `json:"placeholder,omitempty"` //Optional.
Optional bool `json:"optional,omitempty"` //Optional. Default value is false
Value string `json:"value,omitempty"` //Optional.
DataSource string `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external".
SelectedOptions string `json:"selected_options,omitempty"` //Optional. Default value for "external" only
Options []DialogElementOption `json:"options,omitempty"` //One of options or option_groups is required.
OptionGroups []DialogElementOption `json:"option_groups,omitempty"` //Provide up to 100 options.
// DialogOpenResponse response from `dialog.open`
type DialogOpenResponse struct {
SlackResponse
DialogResponseMetadata DialogResponseMetadata `json:"response_metadata"`
}
type DialogElementOption struct {
Label string `json:"label"` //Required.
Value string `json:"value"` //Required.
// DialogResponseMetadata lists the error messages
type DialogResponseMetadata struct {
Messages []string `json:"messages"`
}
// DialogCallback is sent from Slack when a user submits a form from within a dialog
type DialogCallback struct {
Type string `json:"type"`
CallbackID string `json:"callback_id"`
Team Team `json:"team"`
Channel Channel `json:"channel"`
User User `json:"user"`
ActionTs string `json:"action_ts"`
Token string `json:"token"`
ResponseURL string `json:"response_url"`
Submission map[string]string `json:"submission"`
// DialogInputValidationError is an error when user inputs incorrect value to form from within a dialog
type DialogInputValidationError struct {
Name string `json:"name"`
Error string `json:"error"`
}
// DialogSuggestionCallback is sent from Slack when a user types in a select field with an external data source
type DialogSuggestionCallback struct {
Type string `json:"type"`
Token string `json:"token"`
ActionTs string `json:"action_ts"`
Team Team `json:"team"`
User User `json:"user"`
Channel Channel `json:"channel"`
ElementName string `json:"name"`
Value string `json:"value"`
CallbackID string `json:"callback_id"`
// DialogInputValidationErrors lists the name of field and that error messages
type DialogInputValidationErrors struct {
Errors []DialogInputValidationError `json:"errors"`
}
// OpenDialog opens a dialog window where the triggerId originated from
func (api *Client) OpenDialog(triggerId string, dialog Dialog) (err error) {
return api.OpenDialogContext(context.Background(), triggerId, dialog)
// OpenDialog opens a dialog window where the triggerID originated from.
// EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable.
func (api *Client) OpenDialog(triggerID string, dialog Dialog) (err error) {
return api.OpenDialogContext(context.Background(), triggerID, dialog)
}
// OpenDialogContext opens a dialog window where the triggerId originated from with a custom context
func (api *Client) OpenDialogContext(ctx context.Context, triggerId string, dialog Dialog) (err error) {
if triggerId == "" {
return errors.New("received empty parameters")
// EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable.
func (api *Client) OpenDialogContext(ctx context.Context, triggerID string, dialog Dialog) (err error) {
if triggerID == "" {
return ErrParametersMissing
}
resp := DialogTrigger{
TriggerId: triggerId,
req := DialogTrigger{
TriggerID: triggerID,
Dialog: dialog,
}
jsonResp, err := json.Marshal(resp)
encoded, err := json.Marshal(req)
if err != nil {
return err
}
response := &SlackResponse{}
endpoint := SLACK_API + "dialog.open"
if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonResp, response, api.debug); err != nil {
response := &DialogOpenResponse{}
endpoint := api.endpoint + "dialog.open"
if err := postJSON(ctx, api.httpclient, endpoint, api.token, encoded, response, api); err != nil {
return err
}
if len(response.DialogResponseMetadata.Messages) > 0 {
response.Ok = false
response.Error += "\n" + strings.Join(response.DialogResponseMetadata.Messages, "\n")
}
return response.Err()
}

101
vendor/github.com/nlopes/slack/dialog_select.go generated vendored Normal file
View File

@ -0,0 +1,101 @@
package slack
// SelectDataSource types of select datasource
type SelectDataSource string
const (
// DialogDataSourceStatic menu with static Options/OptionGroups
DialogDataSourceStatic SelectDataSource = "static"
// DialogDataSourceExternal dynamic datasource
DialogDataSourceExternal SelectDataSource = "external"
// DialogDataSourceConversations provides a list of conversations
DialogDataSourceConversations SelectDataSource = "conversations"
// DialogDataSourceChannels provides a list of channels
DialogDataSourceChannels SelectDataSource = "channels"
// DialogDataSourceUsers provides a list of users
DialogDataSourceUsers SelectDataSource = "users"
)
// DialogInputSelect dialog support for select boxes.
type DialogInputSelect struct {
DialogInput
Value string `json:"value,omitempty"` //Optional.
DataSource SelectDataSource `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external".
SelectedOptions string `json:"selected_options,omitempty"` //Optional. Default value for "external" only
Options []DialogSelectOption `json:"options,omitempty"` //One of options or option_groups is required.
OptionGroups []DialogOptionGroup `json:"option_groups,omitempty"` //Provide up to 100 options.
MinQueryLength int `json:"min_query_length,omitempty"` //Optional. minimum characters before query is sent.
Hint string `json:"hint,omitempty"` //Optional. Additional hint text.
}
// DialogSelectOption is an option for the user to select from the menu
type DialogSelectOption struct {
Label string `json:"label"`
Value string `json:"value"`
}
// DialogOptionGroup is a collection of options for creating a segmented table
type DialogOptionGroup struct {
Label string `json:"label"`
Options []DialogSelectOption `json:"options"`
}
// NewStaticSelectDialogInput constructor for a `static` datasource menu input
func NewStaticSelectDialogInput(name, label string, options []DialogSelectOption) *DialogInputSelect {
return &DialogInputSelect{
DialogInput: DialogInput{
Type: InputTypeSelect,
Name: name,
Label: label,
Optional: true,
},
DataSource: DialogDataSourceStatic,
Options: options,
}
}
// NewGroupedSelectDialogInput creates grouped options select input for Dialogs.
func NewGroupedSelectDialogInput(name, label string, options []DialogOptionGroup) *DialogInputSelect {
return &DialogInputSelect{
DialogInput: DialogInput{
Type: InputTypeSelect,
Name: name,
Label: label,
},
DataSource: DialogDataSourceStatic,
OptionGroups: options}
}
// NewDialogOptionGroup creates a DialogOptionGroup from several select options
func NewDialogOptionGroup(label string, options ...DialogSelectOption) DialogOptionGroup {
return DialogOptionGroup{
Label: label,
Options: options,
}
}
// NewConversationsSelect returns a `Conversations` select
func NewConversationsSelect(name, label string) *DialogInputSelect {
return newPresetSelect(name, label, DialogDataSourceConversations)
}
// NewChannelsSelect returns a `Channels` select
func NewChannelsSelect(name, label string) *DialogInputSelect {
return newPresetSelect(name, label, DialogDataSourceChannels)
}
// NewUsersSelect returns a `Users` select
func NewUsersSelect(name, label string) *DialogInputSelect {
return newPresetSelect(name, label, DialogDataSourceUsers)
}
func newPresetSelect(name, label string, dataSourceType SelectDataSource) *DialogInputSelect {
return &DialogInputSelect{
DialogInput: DialogInput{
Type: InputTypeSelect,
Label: label,
Name: name,
},
DataSource: dataSourceType,
}
}

50
vendor/github.com/nlopes/slack/dialog_text.go generated vendored Normal file
View File

@ -0,0 +1,50 @@
package slack
// TextInputSubtype Accepts email, number, tel, or url. In some form factors, optimized input is provided for this subtype.
type TextInputSubtype string
const (
// InputSubtypeEmail email keyboard
InputSubtypeEmail TextInputSubtype = "email"
// InputSubtypeNumber numeric keyboard
InputSubtypeNumber TextInputSubtype = "number"
// InputSubtypeTel Phone keyboard
InputSubtypeTel TextInputSubtype = "tel"
// InputSubtypeURL Phone keyboard
InputSubtypeURL TextInputSubtype = "url"
)
// TextInputElement subtype of DialogInput
// https://api.slack.com/dialogs#option_element_attributes#text_element_attributes
type TextInputElement struct {
DialogInput
MaxLength int `json:"max_length,omitempty"`
MinLength int `json:"min_length,omitempty"`
Hint string `json:"hint,omitempty"`
Subtype TextInputSubtype `json:"subtype"`
Value string `json:"value"`
}
// NewTextInput constructor for a `text` input
func NewTextInput(name, label, text string) *TextInputElement {
return &TextInputElement{
DialogInput: DialogInput{
Type: InputTypeText,
Name: name,
Label: label,
},
Value: text,
}
}
// NewTextAreaInput constructor for a `textarea` input
func NewTextAreaInput(name, label, text string) *TextInputElement {
return &TextInputElement{
DialogInput: DialogInput{
Type: InputTypeTextArea,
Name: name,
Label: label,
},
Value: text,
}
}

View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
"strings"
@ -36,16 +35,14 @@ type dndTeamInfoResponse struct {
SlackResponse
}
func dndRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*dndResponseFull, error) {
func (api *Client) dndRequest(ctx context.Context, path string, values url.Values) (*dndResponseFull, error) {
response := &dndResponseFull{}
err := postSlackMethod(ctx, client, path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
// EndDND ends the user's scheduled Do Not Disturb session
@ -61,7 +58,7 @@ func (api *Client) EndDNDContext(ctx context.Context) error {
response := &SlackResponse{}
if err := postSlackMethod(ctx, api.httpclient, "dnd.endDnd", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "dnd.endDnd", values, response); err != nil {
return err
}
@ -79,7 +76,7 @@ func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) {
"token": {api.token},
}
response, err := dndRequest(ctx, api.httpclient, "dnd.endSnooze", values, api.debug)
response, err := api.dndRequest(ctx, "dnd.endSnooze", values)
if err != nil {
return nil, err
}
@ -100,7 +97,7 @@ func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDSta
values.Set("user", *user)
}
response, err := dndRequest(ctx, api.httpclient, "dnd.info", values, api.debug)
response, err := api.dndRequest(ctx, "dnd.info", values)
if err != nil {
return nil, err
}
@ -120,12 +117,14 @@ func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (m
}
response := &dndTeamInfoResponse{}
if err := postSlackMethod(ctx, api.httpclient, "dnd.teamInfo", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "dnd.teamInfo", values, response); err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
if response.Err() != nil {
return nil, response.Err()
}
return response.Users, nil
}
@ -136,7 +135,7 @@ func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) {
return api.SetSnoozeContext(context.Background(), minutes)
}
// SetSnooze adjusts the snooze duration for a user's Do Not Disturb settings with a custom context.
// SetSnoozeContext adjusts the snooze duration for a user's Do Not Disturb settings with a custom context.
// For more information see the SetSnooze docs
func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) {
values := url.Values{
@ -144,7 +143,7 @@ func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatu
"num_minutes": {strconv.Itoa(minutes)},
}
response, err := dndRequest(ctx, api.httpclient, "dnd.setSnooze", values, api.debug)
response, err := api.dndRequest(ctx, "dnd.setSnooze", values)
if err != nil {
return nil, err
}

View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
)
@ -23,12 +22,14 @@ func (api *Client) GetEmojiContext(ctx context.Context) (map[string]string, erro
}
response := &emojiResponseFull{}
err := postSlackMethod(ctx, api.httpclient, "emoji.list", values, response, api.debug)
err := api.postMethod(ctx, "emoji.list", values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
if response.Err() != nil {
return nil, response.Err()
}
return response.Emoji, nil
}

18
vendor/github.com/nlopes/slack/errors.go generated vendored Normal file
View File

@ -0,0 +1,18 @@
package slack
import "github.com/nlopes/slack/internal/errorsx"
// Errors returned by various methods.
const (
ErrAlreadyDisconnected = errorsx.String("Invalid call to Disconnect - Slack API is already disconnected")
ErrRTMDisconnected = errorsx.String("disconnect received while trying to connect")
ErrParametersMissing = errorsx.String("received empty parameters")
ErrInvalidConfiguration = errorsx.String("invalid configuration")
ErrMissingHeaders = errorsx.String("missing headers")
ErrExpiredTimestamp = errorsx.String("timestamp is too old")
)
// internal errors
const (
errPaginationComplete = errorsx.String("pagination complete")
)

View File

@ -2,7 +2,7 @@ package slack
import (
"context"
"errors"
"fmt"
"io"
"net/url"
"strconv"
@ -86,21 +86,40 @@ type File struct {
CommentsCount int `json:"comments_count"`
NumStars int `json:"num_stars"`
IsStarred bool `json:"is_starred"`
Shares Share `json:"shares"`
}
type Share struct {
Public map[string][]ShareFileInfo `json:"public"`
}
type ShareFileInfo struct {
ReplyUsers []string `json:"reply_users"`
ReplyUsersCount int `json:"reply_users_count"`
ReplyCount int `json:"reply_count"`
Ts string `json:"ts"`
ThreadTs string `json:"thread_ts"`
LatestReply string `json:"latest_reply"`
ChannelName string `json:"channel_name"`
TeamID string `json:"team_id"`
}
// FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request.
//
// There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large,
// or provide a local file path in File to upload it from your filesystem.
//
// Note that when using the Reader option, you *must* specify the Filename, otherwise the Slack API isn't happy.
type FileUploadParameters struct {
File string
Content string
Reader io.Reader
Filetype string
Filename string
Title string
InitialComment string
Channels []string
File string
Content string
Reader io.Reader
Filetype string
Filename string
Title string
InitialComment string
Channels []string
ThreadTimestamp string
}
// GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request
@ -136,16 +155,14 @@ func NewGetFilesParameters() GetFilesParameters {
}
}
func fileRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*fileResponseFull, error) {
func (api *Client) fileRequest(ctx context.Context, path string, values url.Values) (*fileResponseFull, error) {
response := &fileResponseFull{}
err := postForm(ctx, client, SLACK_API+path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
// GetFileInfo retrieves a file and related comments
@ -162,13 +179,18 @@ func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count,
"page": {strconv.Itoa(page)},
}
response, err := fileRequest(ctx, api.httpclient, "files.info", values, api.debug)
response, err := api.fileRequest(ctx, "files.info", values)
if err != nil {
return nil, nil, nil, err
}
return &response.File, response.Comments, &response.Paging, nil
}
// 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)
}
// GetFiles retrieves all files according to the parameters given
func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) {
return api.GetFilesContext(context.Background(), params)
@ -201,7 +223,7 @@ func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameter
values.Add("page", strconv.Itoa(params.Page))
}
response, err := fileRequest(ctx, api.httpclient, "files.list", values, api.debug)
response, err := api.fileRequest(ctx, "files.list", values)
if err != nil {
return nil, nil, err
}
@ -221,6 +243,9 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam
if err != nil {
return nil, err
}
if params.Filename == "" {
return nil, fmt.Errorf("files.upload: FileUploadParameters.Filename is mandatory")
}
response := &fileResponseFull{}
values := url.Values{
"token": {api.token},
@ -237,24 +262,25 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam
if params.InitialComment != "" {
values.Add("initial_comment", params.InitialComment)
}
if params.ThreadTimestamp != "" {
values.Add("thread_ts", params.ThreadTimestamp)
}
if len(params.Channels) != 0 {
values.Add("channels", strings.Join(params.Channels, ","))
}
if params.Content != "" {
values.Add("content", params.Content)
err = postForm(ctx, api.httpclient, SLACK_API+"files.upload", values, response, api.debug)
err = api.postMethod(ctx, "files.upload", values, response)
} else if params.File != "" {
err = postLocalWithMultipartResponse(ctx, api.httpclient, "files.upload", params.File, "file", values, response, api.debug)
err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", values, response, api)
} else if params.Reader != nil {
err = postWithMultipartResponse(ctx, api.httpclient, "files.upload", params.Filename, "file", values, params.Reader, response, api.debug)
err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", values, params.Reader, response, api)
}
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return &response.File, nil
return &response.File, response.Err()
}
// DeleteFileComment deletes a file's comment
@ -265,7 +291,7 @@ func (api *Client) DeleteFileComment(commentID, fileID string) error {
// DeleteFileCommentContext deletes a file's comment with a custom context
func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, commentID string) (err error) {
if fileID == "" || commentID == "" {
return errors.New("received empty parameters")
return ErrParametersMissing
}
values := url.Values{
@ -273,7 +299,7 @@ func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, comment
"file": {fileID},
"id": {commentID},
}
_, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api.debug)
_, err = api.fileRequest(ctx, "files.comments.delete", values)
return err
}
@ -289,7 +315,7 @@ func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err er
"file": {fileID},
}
_, err = fileRequest(ctx, api.httpclient, "files.delete", values, api.debug)
_, err = api.fileRequest(ctx, "files.delete", values)
return err
}
@ -305,7 +331,7 @@ func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string
"file": {fileID},
}
response, err := fileRequest(ctx, api.httpclient, "files.revokePublicURL", values, api.debug)
response, err := api.fileRequest(ctx, "files.revokePublicURL", values)
if err != nil {
return nil, err
}
@ -324,7 +350,7 @@ func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string)
"file": {fileID},
}
response, err := fileRequest(ctx, api.httpclient, "files.sharedPublicURL", values, api.debug)
response, err := api.fileRequest(ctx, "files.sharedPublicURL", values)
if err != nil {
return nil, nil, nil, err
}

9
vendor/github.com/nlopes/slack/go.mod generated vendored Normal file
View File

@ -0,0 +1,9 @@
module github.com/nlopes/slack
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gorilla/websocket v1.2.0
github.com/pkg/errors v0.8.0
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.2
)

22
vendor/github.com/nlopes/slack/go.sum generated vendored Normal file
View File

@ -0,0 +1,22 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/nlopes/slack v0.1.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0=
github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/victorcoder/slack-test v0.0.0-20190131110821-6f9a569c10af h1:JFxr+No3ZWgCtxnnTWCybnB/z0Iy3qLmdj3u2NV5o48=
github.com/victorcoder/slack-test v0.0.0-20190131110821-6f9a569c10af/go.mod h1:dStM4ShMus8J3hiq66ExbbzGLkwyZ+RQJePwFhWCCvQ=
github.com/victorcoder/slack-test v0.0.0-20190131113129-a43b3bb77f43 h1:wtFekkaAAQibpy3iE4Hhx2Gi9pZAbITOSfVP7GXk5eM=
github.com/victorcoder/slack-test v0.0.0-20190131113129-a43b3bb77f43/go.mod h1:dStM4ShMus8J3hiq66ExbbzGLkwyZ+RQJePwFhWCCvQ=
golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37 h1:BkNcmLtAVeWe9h5k0jt24CQgaG5vb4x/doFbAiEC/Ho=
golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

View File

@ -2,14 +2,13 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
// Group contains all the information for a group
type Group struct {
groupConversation
GroupConversation
IsGroup bool `json:"is_group"`
}
@ -28,16 +27,14 @@ type groupResponseFull struct {
SlackResponse
}
func groupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*groupResponseFull, error) {
func (api *Client) groupRequest(ctx context.Context, path string, values url.Values) (*groupResponseFull, error) {
response := &groupResponseFull{}
err := postForm(ctx, client, SLACK_API+path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
// ArchiveGroup archives a private group
@ -52,7 +49,7 @@ func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error
"channel": {group},
}
_, err := groupRequest(ctx, api.httpclient, "groups.archive", values, api.debug)
_, err := api.groupRequest(ctx, "groups.archive", values)
return err
}
@ -68,7 +65,7 @@ func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) erro
"channel": {group},
}
_, err := groupRequest(ctx, api.httpclient, "groups.unarchive", values, api.debug)
_, err := api.groupRequest(ctx, "groups.unarchive", values)
return err
}
@ -84,7 +81,7 @@ func (api *Client) CreateGroupContext(ctx context.Context, group string) (*Group
"name": {group},
}
response, err := groupRequest(ctx, api.httpclient, "groups.create", values, api.debug)
response, err := api.groupRequest(ctx, "groups.create", values)
if err != nil {
return nil, err
}
@ -109,32 +106,13 @@ func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (*
"channel": {group},
}
response, err := groupRequest(ctx, api.httpclient, "groups.createChild", values, api.debug)
response, err := api.groupRequest(ctx, "groups.createChild", values)
if err != nil {
return nil, err
}
return &response.Group, nil
}
// CloseGroup closes a private group
func (api *Client) CloseGroup(group string) (bool, bool, error) {
return api.CloseGroupContext(context.Background(), group)
}
// CloseGroupContext closes a private group with a custom context
func (api *Client) CloseGroupContext(ctx context.Context, group string) (bool, bool, error) {
values := url.Values{
"token": {api.token},
"channel": {group},
}
response, err := imRequest(ctx, api.httpclient, "groups.close", values, api.debug)
if err != nil {
return false, false, err
}
return response.NoOp, response.AlreadyClosed, nil
}
// GetGroupHistory fetches all the history for a private group
func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*History, error) {
return api.GetGroupHistoryContext(context.Background(), group, params)
@ -170,7 +148,7 @@ func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, par
}
}
response, err := groupRequest(ctx, api.httpclient, "groups.history", values, api.debug)
response, err := api.groupRequest(ctx, "groups.history", values)
if err != nil {
return nil, err
}
@ -190,7 +168,7 @@ func (api *Client) InviteUserToGroupContext(ctx context.Context, group, user str
"user": {user},
}
response, err := groupRequest(ctx, api.httpclient, "groups.invite", values, api.debug)
response, err := api.groupRequest(ctx, "groups.invite", values)
if err != nil {
return nil, false, err
}
@ -209,7 +187,7 @@ func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err err
"channel": {group},
}
_, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug)
_, err = api.groupRequest(ctx, "groups.leave", values)
return err
}
@ -226,7 +204,7 @@ func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user str
"user": {user},
}
_, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug)
_, err = api.groupRequest(ctx, "groups.kick", values)
return err
}
@ -244,7 +222,7 @@ func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) (
values.Add("exclude_archived", "1")
}
response, err := groupRequest(ctx, api.httpclient, "groups.list", values, api.debug)
response, err := api.groupRequest(ctx, "groups.list", values)
if err != nil {
return nil, err
}
@ -259,11 +237,12 @@ func (api *Client) GetGroupInfo(group string) (*Group, error) {
// GetGroupInfoContext retrieves the given group with a custom context
func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Group, error) {
values := url.Values{
"token": {api.token},
"channel": {group},
"token": {api.token},
"channel": {group},
"include_locale": {strconv.FormatBool(true)},
}
response, err := groupRequest(ctx, api.httpclient, "groups.info", values, api.debug)
response, err := api.groupRequest(ctx, "groups.info", values)
if err != nil {
return nil, err
}
@ -288,7 +267,7 @@ func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string
"ts": {ts},
}
_, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug)
_, err = api.groupRequest(ctx, "groups.mark", values)
return err
}
@ -304,7 +283,7 @@ func (api *Client) OpenGroupContext(ctx context.Context, group string) (bool, bo
"channel": {group},
}
response, err := groupRequest(ctx, api.httpclient, "groups.open", values, api.debug)
response, err := api.groupRequest(ctx, "groups.open", values)
if err != nil {
return false, false, err
}
@ -328,7 +307,7 @@ func (api *Client) RenameGroupContext(ctx context.Context, group, name string) (
// XXX: the created entry in this call returns a string instead of a number
// so I may have to do some workaround to solve it.
response, err := groupRequest(ctx, api.httpclient, "groups.rename", values, api.debug)
response, err := api.groupRequest(ctx, "groups.rename", values)
if err != nil {
return nil, err
}
@ -348,7 +327,7 @@ func (api *Client) SetGroupPurposeContext(ctx context.Context, group, purpose st
"purpose": {purpose},
}
response, err := groupRequest(ctx, api.httpclient, "groups.setPurpose", values, api.debug)
response, err := api.groupRequest(ctx, "groups.setPurpose", values)
if err != nil {
return "", err
}
@ -368,7 +347,7 @@ func (api *Client) SetGroupTopicContext(ctx context.Context, group, topic string
"topic": {topic},
}
response, err := groupRequest(ctx, api.httpclient, "groups.setTopic", values, api.debug)
response, err := api.groupRequest(ctx, "groups.setTopic", values)
if err != nil {
return "", err
}

27
vendor/github.com/nlopes/slack/im.go generated vendored
View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
@ -23,22 +22,18 @@ type imResponseFull struct {
// IM contains information related to the Direct Message channel
type IM struct {
conversation
IsIM bool `json:"is_im"`
User string `json:"user"`
IsUserDeleted bool `json:"is_user_deleted"`
Conversation
IsUserDeleted bool `json:"is_user_deleted"`
}
func imRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*imResponseFull, error) {
func (api *Client) imRequest(ctx context.Context, path string, values url.Values) (*imResponseFull, error) {
response := &imResponseFull{}
err := postSlackMethod(ctx, client, path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
// CloseIMChannel closes the direct message channel
@ -53,7 +48,7 @@ func (api *Client) CloseIMChannelContext(ctx context.Context, channel string) (b
"channel": {channel},
}
response, err := imRequest(ctx, api.httpclient, "im.close", values, api.debug)
response, err := api.imRequest(ctx, "im.close", values)
if err != nil {
return false, false, err
}
@ -74,7 +69,7 @@ func (api *Client) OpenIMChannelContext(ctx context.Context, user string) (bool,
"user": {user},
}
response, err := imRequest(ctx, api.httpclient, "im.open", values, api.debug)
response, err := api.imRequest(ctx, "im.open", values)
if err != nil {
return false, false, "", err
}
@ -94,7 +89,7 @@ func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string)
"ts": {ts},
}
_, err := imRequest(ctx, api.httpclient, "im.mark", values, api.debug)
_, err := api.imRequest(ctx, "im.mark", values)
return err
}
@ -133,7 +128,7 @@ func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, para
}
}
response, err := imRequest(ctx, api.httpclient, "im.history", values, api.debug)
response, err := api.imRequest(ctx, "im.history", values)
if err != nil {
return nil, err
}
@ -151,7 +146,7 @@ func (api *Client) GetIMChannelsContext(ctx context.Context) ([]IM, error) {
"token": {api.token},
}
response, err := imRequest(ctx, api.httpclient, "im.list", values, api.debug)
response, err := api.imRequest(ctx, "im.list", values)
if err != nil {
return nil, err
}

97
vendor/github.com/nlopes/slack/interactions.go generated vendored Normal file
View File

@ -0,0 +1,97 @@
package slack
import (
"encoding/json"
)
// InteractionType type of interactions
type InteractionType string
// ActionType type represents the type of action (attachment, block, etc.)
type actionType string
// action is an interface that should be implemented by all callback action types
type action interface {
actionType() actionType
}
// Types of interactions that can be received.
const (
InteractionTypeDialogCancellation = InteractionType("dialog_cancellation")
InteractionTypeDialogSubmission = InteractionType("dialog_submission")
InteractionTypeDialogSuggestion = InteractionType("dialog_suggestion")
InteractionTypeInteractionMessage = InteractionType("interactive_message")
InteractionTypeMessageAction = InteractionType("message_action")
)
// 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"`
DialogSubmissionCallback
}
// 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 {
AttachmentActions []*AttachmentAction
BlockActions []*BlockAction
}
// UnmarshalJSON implements the Marshaller interface in order to delegate
// marshalling and allow for proper type assertion when decoding the response
func (a *ActionCallbacks) UnmarshalJSON(data []byte) error {
var raw []json.RawMessage
err := json.Unmarshal(data, &raw)
if err != nil {
return err
}
for _, r := range raw {
var obj map[string]interface{}
err := json.Unmarshal(r, &obj)
if err != nil {
return err
}
if _, ok := obj["block_id"].(string); ok {
action, err := unmarshalAction(r, &BlockAction{})
if err != nil {
return err
}
a.BlockActions = append(a.BlockActions, action.(*BlockAction))
return nil
}
action, err := unmarshalAction(r, &AttachmentAction{})
if err != nil {
return err
}
a.AttachmentActions = append(a.AttachmentActions, action.(*AttachmentAction))
}
return nil
}
func unmarshalAction(r json.RawMessage, callbackAction action) (action, error) {
err := json.Unmarshal(r, callbackAction)
if err != nil {
return nil, err
}
return callbackAction, nil
}

View File

@ -0,0 +1,8 @@
package errorsx
// String representing an error, useful for declaring string constants as errors.
type String string
func (t String) Error() string {
return string(t)
}

18
vendor/github.com/nlopes/slack/internal/timex/timex.go generated vendored Normal file
View File

@ -0,0 +1,18 @@
package timex
import "time"
// Max returns the maximum duration
func Max(values ...time.Duration) time.Duration {
var (
max time.Duration
)
for _, v := range values {
if v > max {
max = v
}
}
return max
}

View File

@ -2,52 +2,59 @@ package slack
import (
"fmt"
"sync"
)
// SetLogger let's library users supply a logger, so that api debugging
// can be logged along with the application's debugging info.
func SetLogger(l logProvider) {
loggerMutex.Lock()
logger = ilogger{logProvider: l}
loggerMutex.Unlock()
}
var (
loggerMutex = new(sync.Mutex)
logger logInternal // A logger that can be set by consumers
)
// logProvider is a logger interface compatible with both stdlib and some
// 3rd party loggers such as logrus.
type logProvider interface {
// logger is a logger interface compatible with both stdlib and some
// 3rd party loggers.
type logger interface {
Output(int, string) error
}
// logInternal represents the internal logging api we use.
type logInternal interface {
// ilogger represents the internal logging api we use.
type ilogger interface {
logger
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
Output(int, string) error
}
// ilogger implements the additional methods used by our internal logging.
type ilogger struct {
logProvider
type debug interface {
Debug() bool
// Debugf print a formatted debug line.
Debugf(format string, v ...interface{})
// Debugln print a debug line.
Debugln(v ...interface{})
}
// internalLog implements the additional methods used by our internal logging.
type internalLog struct {
logger
}
// Println replicates the behaviour of the standard logger.
func (t ilogger) Println(v ...interface{}) {
func (t internalLog) Println(v ...interface{}) {
t.Output(2, fmt.Sprintln(v...))
}
// Printf replicates the behaviour of the standard logger.
func (t ilogger) Printf(format string, v ...interface{}) {
func (t internalLog) Printf(format string, v ...interface{}) {
t.Output(2, fmt.Sprintf(format, v...))
}
// Print replicates the behaviour of the standard logger.
func (t ilogger) Print(v ...interface{}) {
func (t internalLog) Print(v ...interface{}) {
t.Output(2, fmt.Sprint(v...))
}
type discard struct{}
func (t discard) Debug() bool {
return false
}
// Debugf print a formatted debug line.
func (t discard) Debugf(format string, v ...interface{}) {}
// Debugln print a debug line.
func (t discard) Debugln(v ...interface{}) {}

View File

@ -4,17 +4,19 @@ package slack
type OutgoingMessage struct {
ID int `json:"id"`
// channel ID
Channel string `json:"channel,omitempty"`
Text string `json:"text,omitempty"`
Type string `json:"type,omitempty"`
ThreadTimestamp string `json:"thread_ts,omitempty"`
ThreadBroadcast bool `json:"reply_broadcast,omitempty"`
Channel string `json:"channel,omitempty"`
Text string `json:"text,omitempty"`
Type string `json:"type,omitempty"`
ThreadTimestamp string `json:"thread_ts,omitempty"`
ThreadBroadcast bool `json:"reply_broadcast,omitempty"`
IDs []string `json:"ids,omitempty"`
}
// Message is an auxiliary type to allow us to have a message containing sub messages
type Message struct {
Msg
SubMessage *Msg `json:"message,omitempty"`
PreviousMessage *Msg `json:"previous_message,omitempty"`
}
// Msg contains information about a slack message
@ -91,6 +93,9 @@ type Msg struct {
ResponseType string `json:"response_type,omitempty"`
ReplaceOriginal bool `json:"replace_original"`
DeleteOriginal bool `json:"delete_original"`
// Block type Message
Blocks Blocks `json:"blocks,omitempty"`
}
// Icon is used for bot messages
@ -147,6 +152,15 @@ func (rtm *RTM) NewOutgoingMessage(text string, channelID string, options ...RTM
return &msg
}
// NewSubscribeUserPresence prepares an OutgoingMessage that the user can
// use to subscribe presence events for the specified users.
func (rtm *RTM) NewSubscribeUserPresence(ids []string) *OutgoingMessage {
return &OutgoingMessage{
Type: "presence_sub",
IDs: ids,
}
}
// NewTypingMessage prepares an OutgoingMessage that the user can
// use to send as a typing indicator. Use this function to properly set the
// messageID.
@ -174,5 +188,4 @@ func RTMsgOptionBroadcast() RTMsgOption {
return func(msg *OutgoingMessage) {
msg.ThreadBroadcast = true
}
}

View File

@ -19,6 +19,7 @@ import (
"time"
)
// SlackResponse handles parsing out errors from the web api.
type SlackResponse struct {
Ok bool `json:"ok"`
Error string `json:"error"`
@ -47,64 +48,89 @@ type statusCodeError struct {
}
func (t statusCodeError) Error() string {
// TODO: this is a bad error string, should clean it up with a breaking changes
// merger.
return fmt.Sprintf("Slack server error: %s.", t.Status)
return fmt.Sprintf("slack server error: %s", t.Status)
}
func (t statusCodeError) HTTPStatusCode() int {
return t.Code
}
func (t statusCodeError) Retryable() bool {
if t.Code >= 500 || t.Code == http.StatusTooManyRequests {
return true
}
return false
}
// RateLimitedError represents the rate limit respond from slack
type RateLimitedError struct {
RetryAfter time.Duration
}
func (e *RateLimitedError) Error() string {
return fmt.Sprintf("Slack rate limit exceeded, retry after %s", e.RetryAfter)
return fmt.Sprintf("slack rate limit exceeded, retry after %s", e.RetryAfter)
}
func fileUploadReq(ctx context.Context, path, fieldname, filename string, values url.Values, r io.Reader) (*http.Request, error) {
body := &bytes.Buffer{}
wr := multipart.NewWriter(body)
func (e *RateLimitedError) Retryable() bool {
return true
}
ioWriter, err := wr.CreateFormFile(fieldname, filename)
func fileUploadReq(ctx context.Context, path string, values url.Values, r io.Reader) (*http.Request, error) {
req, err := http.NewRequest("POST", path, r)
if err != nil {
wr.Close()
return nil, err
}
_, err = io.Copy(ioWriter, r)
if err != nil {
wr.Close()
return nil, err
}
// Close the multipart writer or the footer won't be written
wr.Close()
req, err := http.NewRequest("POST", path, body)
req = req.WithContext(ctx)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", wr.FormDataContentType())
req.URL.RawQuery = (values).Encode()
return req, nil
}
func parseResponseBody(body io.ReadCloser, intf interface{}, debug bool) error {
func downloadFile(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{})
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 {
return err
}
defer resp.Body.Close()
err = checkStatusCode(resp, d)
if err != nil {
return err
}
_, err = io.Copy(writer, resp.Body)
return err
}
func parseResponseBody(body io.ReadCloser, intf interface{}, d debug) error {
response, err := ioutil.ReadAll(body)
if err != nil {
return err
}
// FIXME: will be api.Debugf
if debug {
logger.Printf("parseResponseBody: %s\n", string(response))
if d.Debug() {
d.Debugln("parseResponseBody", string(response))
}
return json.Unmarshal(response, intf)
}
func postLocalWithMultipartResponse(ctx context.Context, client HTTPRequester, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error {
func postLocalWithMultipartResponse(ctx context.Context, client httpClient, path, fpath, fieldname string, values url.Values, intf interface{}, d debug) error {
fullpath, err := filepath.Abs(fpath)
if err != nil {
return err
@ -114,14 +140,57 @@ func postLocalWithMultipartResponse(ctx context.Context, client HTTPRequester, p
return err
}
defer file.Close()
return postWithMultipartResponse(ctx, client, path, filepath.Base(fpath), fieldname, values, file, intf, debug)
return postWithMultipartResponse(ctx, client, path, filepath.Base(fpath), fieldname, values, file, intf, d)
}
func postWithMultipartResponse(ctx context.Context, client HTTPRequester, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, debug bool) error {
req, err := fileUploadReq(ctx, SLACK_API+path, fieldname, name, values, r)
func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, d debug) error {
pipeReader, pipeWriter := io.Pipe()
wr := multipart.NewWriter(pipeWriter)
errc := make(chan error)
go func() {
defer pipeWriter.Close()
ioWriter, err := wr.CreateFormFile(fieldname, name)
if err != nil {
errc <- err
return
}
_, err = io.Copy(ioWriter, r)
if err != nil {
errc <- err
return
}
if err = wr.Close(); err != nil {
errc <- err
return
}
}()
req, err := fileUploadReq(ctx, path, values, pipeReader)
if err != nil {
return err
}
req.Header.Add("Content-Type", wr.FormDataContentType())
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
err = checkStatusCode(resp, d)
if err != nil {
return err
}
select {
case err = <-errc:
return err
default:
return parseResponseBody(resp.Body, intf, d)
}
}
func doPost(ctx context.Context, client httpClient, req *http.Request, intf interface{}, d debug) error {
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
@ -129,50 +198,16 @@ func postWithMultipartResponse(ctx context.Context, client HTTPRequester, path,
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != http.StatusOK {
logResponse(resp, debug)
return statusCodeError{Code: resp.StatusCode, Status: resp.Status}
}
return parseResponseBody(resp.Body, intf, debug)
}
func doPost(ctx context.Context, client HTTPRequester, req *http.Request, intf interface{}, debug bool) error {
req = req.WithContext(ctx)
resp, err := client.Do(req)
err = checkStatusCode(resp, d)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != http.StatusOK {
logResponse(resp, debug)
return statusCodeError{Code: resp.StatusCode, Status: resp.Status}
}
return parseResponseBody(resp.Body, intf, debug)
return parseResponseBody(resp.Body, intf, d)
}
// post JSON.
func postJSON(ctx context.Context, client HTTPRequester, endpoint, token string, json []byte, intf interface{}, debug bool) error {
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)
if err != nil {
@ -180,38 +215,43 @@ func postJSON(ctx context.Context, client HTTPRequester, endpoint, token string,
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return doPost(ctx, client, req, intf, debug)
return doPost(ctx, client, req, intf, d)
}
// post a url encoded form.
func postForm(ctx context.Context, client HTTPRequester, endpoint string, values url.Values, intf interface{}, debug bool) error {
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)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return doPost(ctx, client, req, intf, debug)
return doPost(ctx, client, req, intf, d)
}
// post to a slack web method.
func postSlackMethod(ctx context.Context, client HTTPRequester, path string, values url.Values, intf interface{}, debug bool) error {
return postForm(ctx, client, SLACK_API+path, values, intf, debug)
func getResource(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d debug) error {
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.URL.RawQuery = values.Encode()
return doPost(ctx, client, req, intf, d)
}
func parseAdminResponse(ctx context.Context, client HTTPRequester, method string, teamName string, values url.Values, intf interface{}, debug bool) error {
endpoint := fmt.Sprintf(SLACK_WEB_API_FORMAT, teamName, method, time.Now().Unix())
return postForm(ctx, client, endpoint, values, intf, debug)
func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf interface{}, d debug) error {
endpoint := fmt.Sprintf(WEBAPIURLFormat, teamName, method, time.Now().Unix())
return postForm(ctx, client, endpoint, values, intf, d)
}
func logResponse(resp *http.Response, debug bool) error {
if debug {
func logResponse(resp *http.Response, d debug) error {
if d.Debug() {
text, err := httputil.DumpResponse(resp, true)
if err != nil {
return err
}
logger.Print(string(text))
d.Debugln(string(text))
}
return nil
@ -225,12 +265,6 @@ func okJSONHandler(rw http.ResponseWriter, r *http.Request) {
rw.Write(response)
}
type errorString string
func (t errorString) Error() string {
return string(t)
}
// timerReset safely reset a timer, see time.Timer.Reset for details.
func timerReset(t *time.Timer, d time.Duration) {
if !t.Stop() {
@ -238,3 +272,21 @@ func timerReset(t *time.Timer, d time.Duration) {
}
t.Reset(d)
}
func checkStatusCode(resp *http.Response, d debug) error {
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != http.StatusOK {
logResponse(resp, d)
return statusCodeError{Code: resp.StatusCode, Status: resp.Status}
}
return nil
}

View File

@ -2,10 +2,10 @@ package slack
import (
"context"
"errors"
"net/url"
)
// OAuthResponseIncomingWebhook ...
type OAuthResponseIncomingWebhook struct {
URL string `json:"url"`
Channel string `json:"channel"`
@ -13,11 +13,13 @@ type OAuthResponseIncomingWebhook struct {
ConfigurationURL string `json:"configuration_url"`
}
// OAuthResponseBot ...
type OAuthResponseBot struct {
BotUserID string `json:"bot_user_id"`
BotAccessToken string `json:"bot_access_token"`
}
// OAuthResponse ...
type OAuthResponse struct {
AccessToken string `json:"access_token"`
Scope string `json:"scope"`
@ -30,24 +32,24 @@ type OAuthResponse struct {
}
// GetOAuthToken retrieves an AccessToken
func GetOAuthToken(clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) {
return GetOAuthTokenContext(context.Background(), clientID, clientSecret, code, redirectURI, debug)
func GetOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) {
return GetOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI)
}
// GetOAuthTokenContext retrieves an AccessToken with a custom context
func GetOAuthTokenContext(ctx context.Context, clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) {
response, err := GetOAuthResponseContext(ctx, clientID, clientSecret, code, redirectURI, debug)
func GetOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) {
response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI)
if err != nil {
return "", "", err
}
return response.AccessToken, response.Scope, nil
}
func GetOAuthResponse(clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) {
return GetOAuthResponseContext(context.Background(), clientID, clientSecret, code, redirectURI, debug)
func GetOAuthResponse(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) {
return GetOAuthResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI)
}
func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) {
func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) {
values := url.Values{
"client_id": {clientID},
"client_secret": {clientSecret},
@ -55,12 +57,8 @@ func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code,
"redirect_uri": {redirectURI},
}
response := &OAuthResponse{}
err = postSlackMethod(ctx, customHTTPClient, "oauth.access", values, response, debug)
if err != nil {
if err = postForm(ctx, client, APIURL+"oauth.access", values, response, discard{}); err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}

View File

@ -34,7 +34,7 @@ func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemR
}
response := &SlackResponse{}
if err := postSlackMethod(ctx, api.httpclient, "pins.add", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "pins.add", values, response); err != nil {
return err
}
@ -63,7 +63,7 @@ func (api *Client) RemovePinContext(ctx context.Context, channel string, item It
}
response := &SlackResponse{}
if err := postSlackMethod(ctx, api.httpclient, "pins.remove", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "pins.remove", values, response); err != nil {
return err
}
@ -83,7 +83,7 @@ func (api *Client) ListPinsContext(ctx context.Context, channel string) ([]Item,
}
response := &listPinsResponseFull{}
err := postSlackMethod(ctx, api.httpclient, "pins.list", values, response, api.debug)
err := api.postMethod(ctx, "pins.list", values, response)
if err != nil {
return nil, nil, err
}

View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
@ -155,7 +154,7 @@ func (api *Client) AddReactionContext(ctx context.Context, name string, item Ite
}
response := &SlackResponse{}
if err := postSlackMethod(ctx, api.httpclient, "reactions.add", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "reactions.add", values, response); err != nil {
return err
}
@ -189,7 +188,7 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item
}
response := &SlackResponse{}
if err := postSlackMethod(ctx, api.httpclient, "reactions.remove", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "reactions.remove", values, response); err != nil {
return err
}
@ -223,12 +222,14 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params
}
response := &getReactionsResponseFull{}
if err := postSlackMethod(ctx, api.httpclient, "reactions.get", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "reactions.get", values, response); err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
if err := response.Err(); err != nil {
return nil, err
}
return response.extractReactions(), nil
}
@ -256,12 +257,14 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction
}
response := &listReactionsResponseFull{}
err := postSlackMethod(ctx, api.httpclient, "reactions.list", values, response, api.debug)
err := api.postMethod(ctx, "reactions.list", values, response)
if err != nil {
return nil, nil, err
}
if !response.Ok {
return nil, nil, errors.New(response.Error)
if err := response.Err(); err != nil {
return nil, nil, err
}
return response.extractReactedItems(), &response.Paging, nil
}

75
vendor/github.com/nlopes/slack/reminders.go generated vendored Normal file
View File

@ -0,0 +1,75 @@
package slack
import (
"context"
"net/url"
"time"
)
type Reminder struct {
ID string `json:"id"`
Creator string `json:"creator"`
User string `json:"user"`
Text string `json:"text"`
Recurring bool `json:"recurring"`
Time time.Time `json:"time"`
CompleteTS int `json:"complete_ts"`
}
type reminderResp struct {
SlackResponse
Reminder Reminder `json:"reminder"`
}
func (api *Client) doReminder(ctx context.Context, path string, values url.Values) (*Reminder, error) {
response := &reminderResp{}
if err := api.postMethod(ctx, path, values, response); err != nil {
return nil, err
}
return &response.Reminder, response.Err()
}
// AddChannelReminder adds a reminder for a channel.
//
// See https://api.slack.com/methods/reminders.add (NOTE: the ability to set
// reminders on a channel is currently undocumented but has been tested to
// work)
func (api *Client) AddChannelReminder(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)
}
// AddUserReminder adds a reminder for a user.
//
// See https://api.slack.com/methods/reminders.add (NOTE: the ability to set
// reminders on a channel is currently undocumented but has been tested to
// work)
func (api *Client) AddUserReminder(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)
}
// DeleteReminder deletes an existing reminder.
//
// See https://api.slack.com/methods/reminders.delete
func (api *Client) DeleteReminder(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 {
return err
}
return response.Err()
}

View File

@ -38,7 +38,7 @@ func (api *Client) StartRTM() (info *Info, websocketURL string, err error) {
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) {
response := &infoResponseFull{}
err = postSlackMethod(ctx, api.httpclient, "rtm.start", url.Values{"token": {api.token}}, response, api.debug)
err = api.postMethod(ctx, "rtm.start", url.Values{"token": {api.token}}, response)
if err != nil {
return nil, "", err
}
@ -63,7 +63,7 @@ func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) {
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) {
response := &infoResponseFull{}
err = postSlackMethod(ctx, api.httpclient, "rtm.connect", url.Values{"token": {api.token}}, response, api.debug)
err = api.postMethod(ctx, "rtm.connect", url.Values{"token": {api.token}}, response)
if err != nil {
api.Debugf("Failed to connect to RTM: %s", err)
return nil, "", err
@ -100,6 +100,13 @@ func RTMOptionPingInterval(d time.Duration) RTMOption {
}
}
// RTMOptionConnParams installs parameters to embed into the connection URL.
func RTMOptionConnParams(connParams url.Values) RTMOption {
return func(rtm *RTM) {
rtm.connParams = connParams
}
}
// NewRTM returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol.
func (api *Client) NewRTM(options ...RTMOption) *RTM {
@ -109,10 +116,9 @@ func (api *Client) NewRTM(options ...RTMOption) *RTM {
outgoingMessages: make(chan OutgoingMessage, 20),
pingInterval: defaultPingInterval,
pingDeadman: time.NewTimer(deadmanDuration(defaultPingInterval)),
isConnected: false,
wasIntentional: true,
killChannel: make(chan bool),
disconnected: make(chan struct{}, 1),
disconnected: make(chan struct{}),
disconnectedm: &sync.Once{},
forcePing: make(chan bool),
rawEvents: make(chan json.RawMessage),
idGen: NewSafeID(1),
@ -125,14 +131,3 @@ func (api *Client) NewRTM(options ...RTMOption) *RTM {
return result
}
// NewRTMWithOptions Deprecated just use NewRTM(RTMOptionsUseStart(true))
// returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol.
// This also allows to configure various options available for RTM API.
func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM {
if options != nil {
return api.NewRTM(RTMOptionUseStart(options.UseRTMStart))
}
return api.NewRTM()
}

View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
@ -104,14 +103,12 @@ func (api *Client) _search(ctx context.Context, path, query string, params Searc
}
response = &searchResponseFull{}
err := postSlackMethod(ctx, api.httpclient, path, values, response, api.debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}

100
vendor/github.com/nlopes/slack/security.go generated vendored Normal file
View File

@ -0,0 +1,100 @@
package slack
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"hash"
"net/http"
"strconv"
"strings"
"time"
)
// Signature headers
const (
hSignature = "X-Slack-Signature"
hTimestamp = "X-Slack-Request-Timestamp"
)
// SecretsVerifier contains the information needed to verify that the request comes from Slack
type SecretsVerifier struct {
signature []byte
hmac hash.Hash
}
func unsafeSignatureVerifier(header http.Header, secret string) (_ SecretsVerifier, err error) {
var (
bsignature []byte
)
signature := header.Get(hSignature)
stimestamp := header.Get(hTimestamp)
if signature == "" || stimestamp == "" {
return SecretsVerifier{}, ErrMissingHeaders
}
if bsignature, err = hex.DecodeString(strings.TrimPrefix(signature, "v0=")); err != nil {
return SecretsVerifier{}, err
}
hash := hmac.New(sha256.New, []byte(secret))
if _, err = hash.Write([]byte(fmt.Sprintf("v0:%s:", stimestamp))); err != nil {
return SecretsVerifier{}, err
}
return SecretsVerifier{
signature: bsignature,
hmac: hash,
}, nil
}
// NewSecretsVerifier returns a SecretsVerifier object in exchange for an http.Header object and signing secret
func NewSecretsVerifier(header http.Header, secret string) (sv SecretsVerifier, err error) {
var (
timestamp int64
)
stimestamp := header.Get(hTimestamp)
if sv, err = unsafeSignatureVerifier(header, secret); err != nil {
return SecretsVerifier{}, err
}
if timestamp, err = strconv.ParseInt(stimestamp, 10, 64); err != nil {
return SecretsVerifier{}, err
}
diff := absDuration(time.Since(time.Unix(timestamp, 0)))
if diff > 5*time.Minute {
return SecretsVerifier{}, ErrExpiredTimestamp
}
return sv, err
}
func (v *SecretsVerifier) Write(body []byte) (n int, err error) {
return v.hmac.Write(body)
}
// Ensure compares the signature sent from Slack with the actual computed hash to judge validity
func (v SecretsVerifier) Ensure() error {
computed := v.hmac.Sum(nil)
// use hmac.Equal prevent leaking timing information.
if hmac.Equal(computed, v.signature) {
return nil
}
return fmt.Errorf("Expected signing signature: %s, but computed: %s", hex.EncodeToString(v.signature), hex.EncodeToString(computed))
}
func abs64(n int64) int64 {
y := n >> 63
return (n ^ y) - y
}
func absDuration(n time.Duration) time.Duration {
return time.Duration(abs64(int64(n)))
}

View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"fmt"
"log"
"net/http"
@ -10,31 +9,18 @@ import (
"os"
)
// Added as a var so that we can change this for testing purposes
var SLACK_API string = "https://slack.com/api/"
var SLACK_WEB_API_FORMAT string = "https://%s.slack.com/api/users.admin.%s?t=%s"
const (
// APIURL of the slack api.
APIURL = "https://slack.com/api/"
// WEBAPIURLFormat ...
WEBAPIURLFormat = "https://%s.slack.com/api/users.admin.%s?t=%d"
)
// HTTPClient sets a custom http.Client
// deprecated: in favor of SetHTTPClient()
var HTTPClient = &http.Client{}
var customHTTPClient HTTPRequester = HTTPClient
// HTTPRequester defines the minimal interface needed for an http.Client to be implemented.
//
// Use it in conjunction with the SetHTTPClient function to allow for other capabilities
// like a tracing http.Client
type HTTPRequester interface {
// httpClient defines the minimal interface needed for an http.Client to be implemented.
type httpClient interface {
Do(*http.Request) (*http.Response, error)
}
// SetHTTPClient allows you to specify a custom http.Client
// Use this instead of the package level HTTPClient variable if you want to use a custom client like the
// Stackdriver Trace HTTPClient https://godoc.org/cloud.google.com/go/trace#HTTPClient
func SetHTTPClient(client HTTPRequester) {
customHTTPClient = client
}
// ResponseMetadata holds pagination metadata
type ResponseMetadata struct {
Cursor string `json:"next_cursor"`
@ -48,12 +34,15 @@ func (t *ResponseMetadata) initialize() *ResponseMetadata {
return &ResponseMetadata{}
}
// AuthTestResponse ...
type AuthTestResponse struct {
URL string `json:"url"`
Team string `json:"team"`
User string `json:"user"`
TeamID string `json:"team_id"`
UserID string `json:"user_id"`
// EnterpriseID is only returned when an enterprise id present
EnterpriseID string `json:"enterprise_id,omitempty"`
}
type authTestResponseFull struct {
@ -61,28 +50,51 @@ type authTestResponseFull struct {
AuthTestResponse
}
// Client for the slack api.
type Client struct {
token string
info Info
endpoint string
debug bool
httpclient HTTPRequester
log ilogger
httpclient httpClient
}
// Option defines an option for a Client
type Option func(*Client)
// OptionHTTPClient - provide a custom http client to the slack client.
func OptionHTTPClient(c HTTPRequester) func(*Client) {
return func(s *Client) {
s.httpclient = c
func OptionHTTPClient(client httpClient) func(*Client) {
return func(c *Client) {
c.httpclient = client
}
}
// OptionDebug enable debugging for the client
func OptionDebug(b bool) func(*Client) {
return func(c *Client) {
c.debug = b
}
}
// OptionLog set logging for client.
func OptionLog(l logger) func(*Client) {
return func(c *Client) {
c.log = internalLog{logger: l}
}
}
// OptionAPIURL set the url for the client. only useful for testing.
func OptionAPIURL(u string) func(*Client) {
return func(c *Client) { c.endpoint = u }
}
// New builds a slack client from the provided token and options.
func New(token string, options ...Option) *Client {
s := &Client{
token: token,
httpclient: customHTTPClient,
endpoint: APIURL,
httpclient: &http.Client{},
log: log.New(os.Stderr, "nlopes/slack", log.LstdFlags|log.Lshortfile),
}
for _, opt := range options {
@ -98,43 +110,42 @@ func (api *Client) AuthTest() (response *AuthTestResponse, error error) {
}
// AuthTestContext tests if the user is able to do authenticated requests or not with a custom context
func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestResponse, error error) {
func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestResponse, err error) {
api.Debugf("Challenging auth...")
responseFull := &authTestResponseFull{}
err := postSlackMethod(ctx, api.httpclient, "auth.test", url.Values{"token": {api.token}}, responseFull, api.debug)
err = api.postMethod(ctx, "auth.test", url.Values{"token": {api.token}}, responseFull)
if err != nil {
api.Debugf("failed to test for auth: %s", err)
return nil, err
}
if !responseFull.Ok {
api.Debugf("auth response was not Ok: %s", responseFull.Error)
return nil, errors.New(responseFull.Error)
}
api.Debugf("Auth challenge was successful with response %+v", responseFull.AuthTestResponse)
return &responseFull.AuthTestResponse, nil
}
// SetDebug switches the api into debug mode
// When in debug mode, it logs various info about what its doing
// If you ever use this in production, don't call SetDebug(true)
func (api *Client) SetDebug(debug bool) {
api.debug = debug
if debug && logger == nil {
SetLogger(log.New(os.Stdout, "nlopes/slack", log.LstdFlags|log.Lshortfile))
}
return &responseFull.AuthTestResponse, responseFull.Err()
}
// Debugf print a formatted debug line.
func (api *Client) Debugf(format string, v ...interface{}) {
if api.debug {
logger.Output(2, fmt.Sprintf(format, v...))
api.log.Output(2, fmt.Sprintf(format, v...))
}
}
// Debugln print a debug line.
func (api *Client) Debugln(v ...interface{}) {
if api.debug {
logger.Output(2, fmt.Sprintln(v...))
api.log.Output(2, fmt.Sprintln(v...))
}
}
// Debug returns if debug is enabled.
func (api *Client) Debug() bool {
return api.debug
}
// post to a slack web method.
func (api *Client) postMethod(ctx context.Context, path string, values url.Values, intf interface{}) error {
return postForm(ctx, api.httpclient, api.endpoint+path, values, intf, api)
}
// get a slack web method.
func (api *Client) getMethod(ctx context.Context, path string, values url.Values, intf interface{}) error {
return getResource(ctx, api.httpclient, api.endpoint+path, values, intf, api)
}

View File

@ -0,0 +1,62 @@
// Package slackutilsx is a utility package that doesn't promise API stability.
// its for experimental functionality and utilities.
package slackutilsx
import (
"strings"
"unicode/utf8"
)
// ChannelType the type of channel based on the channelID
type ChannelType int
func (t ChannelType) String() string {
switch t {
case CTypeDM:
return "Direct"
case CTypeGroup:
return "Group"
case CTypeChannel:
return "Channel"
default:
return "Unknown"
}
}
const (
// CTypeUnknown represents channels we cannot properly detect.
CTypeUnknown ChannelType = iota
// CTypeDM is a private channel between two slack users.
CTypeDM
// CTypeGroup is a group channel.
CTypeGroup
// CTypeChannel is a public channel.
CTypeChannel
)
// DetectChannelType converts a channelID to a ChannelType.
// channelID must not be empty. However, if it is empty, the channel type will default to Unknown.
func DetectChannelType(channelID string) ChannelType {
// intentionally ignore the error and just default to CTypeUnknown
switch r, _ := utf8.DecodeRuneInString(channelID); r {
case 'C':
return CTypeChannel
case 'G':
return CTypeGroup
case 'D':
return CTypeDM
default:
return CTypeUnknown
}
}
// EscapeMessage text
func EscapeMessage(message string) string {
replacer := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
return replacer.Replace(message)
}
// Retryable errors return true.
type Retryable interface {
Retryable() bool
}

View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
@ -58,7 +57,7 @@ func (api *Client) AddStarContext(ctx context.Context, channel string, item Item
}
response := &SlackResponse{}
if err := postSlackMethod(ctx, api.httpclient, "stars.add", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "stars.add", values, response); err != nil {
return err
}
@ -87,7 +86,7 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I
}
response := &SlackResponse{}
if err := postSlackMethod(ctx, api.httpclient, "stars.remove", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "stars.remove", values, response); err != nil {
return err
}
@ -115,13 +114,15 @@ func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters)
}
response := &listResponseFull{}
err := postSlackMethod(ctx, api.httpclient, "stars.list", values, response, api.debug)
err := api.postMethod(ctx, "stars.list", values, response)
if err != nil {
return nil, nil, err
}
if !response.Ok {
return nil, nil, errors.New(response.Error)
if err := response.Err(); err != nil {
return nil, nil, err
}
return response.Items, &response.Paging, nil
}

View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
@ -67,44 +66,33 @@ func NewAccessLogParameters() AccessLogParameters {
}
}
func teamRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*TeamResponse, error) {
func (api *Client) teamRequest(ctx context.Context, path string, values url.Values) (*TeamResponse, error) {
response := &TeamResponse{}
err := postSlackMethod(ctx, client, path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
func billableInfoRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (map[string]BillingActive, error) {
func (api *Client) billableInfoRequest(ctx context.Context, path string, values url.Values) (map[string]BillingActive, error) {
response := &BillableInfoResponse{}
err := postSlackMethod(ctx, client, path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response.BillableInfo, nil
return response.BillableInfo, response.Err()
}
func accessLogsRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*LoginResponse, error) {
func (api *Client) accessLogsRequest(ctx context.Context, path string, values url.Values) (*LoginResponse, error) {
response := &LoginResponse{}
err := postSlackMethod(ctx, client, path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
// GetTeamInfo gets the Team Information of the user
@ -118,7 +106,7 @@ func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) {
"token": {api.token},
}
response, err := teamRequest(ctx, api.httpclient, "team.info", values, api.debug)
response, err := api.teamRequest(ctx, "team.info", values)
if err != nil {
return nil, err
}
@ -142,24 +130,26 @@ func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogPar
values.Add("page", strconv.Itoa(params.Page))
}
response, err := accessLogsRequest(ctx, api.httpclient, "team.accessLogs", values, api.debug)
response, err := api.accessLogsRequest(ctx, "team.accessLogs", values)
if err != nil {
return nil, nil, err
}
return response.Logins, &response.Paging, nil
}
// GetBillableInfo ...
func (api *Client) GetBillableInfo(user string) (map[string]BillingActive, error) {
return api.GetBillableInfoContext(context.Background(), user)
}
// GetBillableInfoContext ...
func (api *Client) GetBillableInfoContext(ctx context.Context, user string) (map[string]BillingActive, error) {
values := url.Values{
"token": {api.token},
"user": {user},
}
return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug)
return api.billableInfoRequest(ctx, "team.billableInfo", values)
}
// GetBillableInfoForTeam returns the billing_active status of all users on the team.
@ -173,5 +163,5 @@ func (api *Client) GetBillableInfoForTeamContext(ctx context.Context) (map[strin
"token": {api.token},
}
return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug)
return api.billableInfoRequest(ctx, "team.billableInfo", values)
}

View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
"strings"
)
@ -25,6 +24,7 @@ type UserGroup struct {
DeletedBy string `json:"deleted_by"`
Prefs UserGroupPrefs `json:"prefs"`
UserCount int `json:"user_count"`
Users []string `json:"users"`
}
// UserGroupPrefs contains default channels and groups (private channels)
@ -40,16 +40,14 @@ type userGroupResponseFull struct {
SlackResponse
}
func userGroupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userGroupResponseFull, error) {
func (api *Client) userGroupRequest(ctx context.Context, path string, values url.Values) (*userGroupResponseFull, error) {
response := &userGroupResponseFull{}
err := postSlackMethod(ctx, client, path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
// CreateUserGroup creates a new user group
@ -76,7 +74,7 @@ func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGro
values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")}
}
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.create", values, api.debug)
response, err := api.userGroupRequest(ctx, "usergroups.create", values)
if err != nil {
return UserGroup{}, err
}
@ -95,7 +93,7 @@ func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string
"usergroup": {userGroup},
}
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.disable", values, api.debug)
response, err := api.userGroupRequest(ctx, "usergroups.disable", values)
if err != nil {
return UserGroup{}, err
}
@ -114,25 +112,71 @@ func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string)
"usergroup": {userGroup},
}
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.enable", values, api.debug)
response, err := api.userGroupRequest(ctx, "usergroups.enable", values)
if err != nil {
return UserGroup{}, err
}
return response.UserGroup, nil
}
// GetUserGroupsOption options for the GetUserGroups method call.
type GetUserGroupsOption func(*GetUserGroupsParams)
// GetUserGroupsOptionIncludeCount include the number of users in each User Group (default: false)
func GetUserGroupsOptionIncludeCount(b bool) GetUserGroupsOption {
return func(params *GetUserGroupsParams) {
params.IncludeCount = b
}
}
// GetUserGroupsOptionIncludeDisabled include disabled User Groups (default: false)
func GetUserGroupsOptionIncludeDisabled(b bool) GetUserGroupsOption {
return func(params *GetUserGroupsParams) {
params.IncludeDisabled = b
}
}
// GetUserGroupsOptionIncludeUsers include the list of users for each User Group (default: false)
func GetUserGroupsOptionIncludeUsers(b bool) GetUserGroupsOption {
return func(params *GetUserGroupsParams) {
params.IncludeUsers = b
}
}
// GetUserGroupsParams contains arguments for GetUserGroups method call
type GetUserGroupsParams struct {
IncludeCount bool
IncludeDisabled bool
IncludeUsers bool
}
// GetUserGroups returns a list of user groups for the team
func (api *Client) GetUserGroups() ([]UserGroup, error) {
return api.GetUserGroupsContext(context.Background())
func (api *Client) GetUserGroups(options ...GetUserGroupsOption) ([]UserGroup, error) {
return api.GetUserGroupsContext(context.Background(), options...)
}
// GetUserGroupsContext returns a list of user groups for the team with a custom context
func (api *Client) GetUserGroupsContext(ctx context.Context) ([]UserGroup, error) {
func (api *Client) GetUserGroupsContext(ctx context.Context, options ...GetUserGroupsOption) ([]UserGroup, error) {
params := GetUserGroupsParams{}
for _, opt := range options {
opt(&params)
}
values := url.Values{
"token": {api.token},
}
if params.IncludeCount {
values.Add("include_count", "true")
}
if params.IncludeDisabled {
values.Add("include_disabled", "true")
}
if params.IncludeUsers {
values.Add("include_users", "true")
}
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.list", values, api.debug)
response, err := api.userGroupRequest(ctx, "usergroups.list", values)
if err != nil {
return nil, err
}
@ -162,8 +206,12 @@ func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGro
if userGroup.Description != "" {
values["description"] = []string{userGroup.Description}
}
if len(userGroup.Prefs.Channels) > 0 {
values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")}
}
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.update", values, api.debug)
response, err := api.userGroupRequest(ctx, "usergroups.update", values)
if err != nil {
return UserGroup{}, err
}
@ -182,7 +230,7 @@ func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup str
"usergroup": {userGroup},
}
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.list", values, api.debug)
response, err := api.userGroupRequest(ctx, "usergroups.users.list", values)
if err != nil {
return []string{}, err
}
@ -202,7 +250,7 @@ func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup
"users": {members},
}
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.update", values, api.debug)
response, err := api.userGroupRequest(ctx, "usergroups.users.update", values)
if err != nil {
return UserGroup{}, err
}

View File

@ -3,7 +3,6 @@ package slack
import (
"context"
"encoding/json"
"errors"
"net/url"
"strconv"
)
@ -12,7 +11,6 @@ const (
DEFAULT_USER_PHOTO_CROP_X = -1
DEFAULT_USER_PHOTO_CROP_Y = -1
DEFAULT_USER_PHOTO_CROP_W = -1
errPaginationComplete = errorString("pagination complete")
)
// UserProfile contains all the information details of a given user
@ -37,6 +35,7 @@ type UserProfile struct {
ApiAppID string `json:"api_app_id,omitempty"`
StatusText string `json:"status_text,omitempty"`
StatusEmoji string `json:"status_emoji,omitempty"`
StatusExpiration int `json:"status_expiration"`
Team string `json:"team"`
Fields UserProfileCustomFields `json:"fields"`
}
@ -100,28 +99,30 @@ type UserProfileCustomField struct {
// User contains all the information of a user
type User struct {
ID string `json:"id"`
TeamID string `json:"team_id"`
Name string `json:"name"`
Deleted bool `json:"deleted"`
Color string `json:"color"`
RealName string `json:"real_name"`
TZ string `json:"tz,omitempty"`
TZLabel string `json:"tz_label"`
TZOffset int `json:"tz_offset"`
Profile UserProfile `json:"profile"`
IsBot bool `json:"is_bot"`
IsAdmin bool `json:"is_admin"`
IsOwner bool `json:"is_owner"`
IsPrimaryOwner bool `json:"is_primary_owner"`
IsRestricted bool `json:"is_restricted"`
IsUltraRestricted bool `json:"is_ultra_restricted"`
IsStranger bool `json:"is_stranger"`
IsAppUser bool `json:"is_app_user"`
Has2FA bool `json:"has_2fa"`
HasFiles bool `json:"has_files"`
Presence string `json:"presence"`
Locale string `json:"locale"`
ID string `json:"id"`
TeamID string `json:"team_id"`
Name string `json:"name"`
Deleted bool `json:"deleted"`
Color string `json:"color"`
RealName string `json:"real_name"`
TZ string `json:"tz,omitempty"`
TZLabel string `json:"tz_label"`
TZOffset int `json:"tz_offset"`
Profile UserProfile `json:"profile"`
IsBot bool `json:"is_bot"`
IsAdmin bool `json:"is_admin"`
IsOwner bool `json:"is_owner"`
IsPrimaryOwner bool `json:"is_primary_owner"`
IsRestricted bool `json:"is_restricted"`
IsUltraRestricted bool `json:"is_ultra_restricted"`
IsStranger bool `json:"is_stranger"`
IsAppUser bool `json:"is_app_user"`
Has2FA bool `json:"has_2fa"`
HasFiles bool `json:"has_files"`
Presence string `json:"presence"`
Locale string `json:"locale"`
Updated JSONTime `json:"updated"`
Enterprise EnterpriseUser `json:"enterprise_user,omitempty"`
}
// UserPresence contains details about a user online status
@ -152,6 +153,17 @@ type UserIdentity struct {
Image512 string `json:"image_512"`
}
// EnterpriseUser is present when a user is part of Slack Enterprise Grid
// https://api.slack.com/types/user#enterprise_grid_user_objects
type EnterpriseUser struct {
ID string `json:"id"`
EnterpriseID string `json:"enterprise_id"`
EnterpriseName string `json:"enterprise_name"`
IsAdmin bool `json:"is_admin"`
IsOwner bool `json:"is_owner"`
Teams []string `json:"teams"`
}
type TeamIdentity struct {
ID string `json:"id"`
Name string `json:"name"`
@ -189,16 +201,14 @@ func NewUserSetPhotoParams() UserSetPhotoParams {
}
}
func userRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userResponseFull, error) {
func (api *Client) userRequest(ctx context.Context, path string, values url.Values) (*userResponseFull, error) {
response := &userResponseFull{}
err := postForm(ctx, client, SLACK_API+path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
// GetUserPresence will retrieve the current presence status of given user.
@ -213,7 +223,7 @@ func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*Us
"user": {user},
}
response, err := userRequest(ctx, api.httpclient, "users.getPresence", values, api.debug)
response, err := api.userRequest(ctx, "users.getPresence", values)
if err != nil {
return nil, err
}
@ -228,11 +238,12 @@ func (api *Client) GetUserInfo(user string) (*User, error) {
// GetUserInfoContext will retrieve the complete user information with a custom context
func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, error) {
values := url.Values{
"token": {api.token},
"user": {user},
"token": {api.token},
"user": {user},
"include_locale": {strconv.FormatBool(true)},
}
response, err := userRequest(ctx, api.httpclient, "users.info", values, api.debug)
response, err := api.userRequest(ctx, "users.info", values)
if err != nil {
return nil, err
}
@ -304,13 +315,14 @@ func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error)
t.previousResp = t.previousResp.initialize()
values := url.Values{
"limit": {strconv.Itoa(t.limit)},
"presence": {strconv.FormatBool(t.presence)},
"token": {t.c.token},
"cursor": {t.previousResp.Cursor},
"limit": {strconv.Itoa(t.limit)},
"presence": {strconv.FormatBool(t.presence)},
"token": {t.c.token},
"cursor": {t.previousResp.Cursor},
"include_locale": {strconv.FormatBool(true)},
}
if resp, err = userRequest(ctx, t.c.httpclient, "users.list", values, t.c.debug); err != nil {
if resp, err = t.c.userRequest(ctx, "users.list", values); err != nil {
return t, err
}
@ -355,7 +367,7 @@ func (api *Client) GetUserByEmailContext(ctx context.Context, email string) (*Us
"token": {api.token},
"email": {email},
}
response, err := userRequest(ctx, api.httpclient, "users.lookupByEmail", values, api.debug)
response, err := api.userRequest(ctx, "users.lookupByEmail", values)
if err != nil {
return nil, err
}
@ -373,7 +385,7 @@ func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) {
"token": {api.token},
}
_, err = userRequest(ctx, api.httpclient, "users.setActive", values, api.debug)
_, err = api.userRequest(ctx, "users.setActive", values)
return err
}
@ -389,7 +401,7 @@ func (api *Client) SetUserPresenceContext(ctx context.Context, presence string)
"presence": {presence},
}
_, err := userRequest(ctx, api.httpclient, "users.setPresence", values, api.debug)
_, err := api.userRequest(ctx, "users.setPresence", values)
return err
}
@ -405,13 +417,15 @@ func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityRes
}
response := &UserIdentityResponse{}
err := postForm(ctx, api.httpclient, SLACK_API+"users.identity", values, response, api.debug)
err := api.postMethod(ctx, "users.identity", values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
if err := response.Err(); err != nil {
return nil, err
}
return response, nil
}
@ -436,7 +450,7 @@ func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params
values.Add("crop_w", strconv.Itoa(params.CropW))
}
err := postLocalWithMultipartResponse(ctx, api.httpclient, "users.setPhoto", image, "image", values, response, api.debug)
err := postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", values, response, api)
if err != nil {
return err
}
@ -456,7 +470,7 @@ func (api *Client) DeleteUserPhotoContext(ctx context.Context) error {
"token": {api.token},
}
err := postForm(ctx, api.httpclient, SLACK_API+"users.deletePhoto", values, response, api.debug)
err := api.postMethod(ctx, "users.deletePhoto", values, response)
if err != nil {
return err
}
@ -467,15 +481,16 @@ func (api *Client) DeleteUserPhotoContext(ctx context.Context) error {
// SetUserCustomStatus will set a custom status and emoji for the currently
// authenticated user. If statusEmoji is "" and statusText is not, the Slack API
// will automatically set it to ":speech_balloon:". Otherwise, if both are ""
// the Slack API will unset the custom status/emoji.
func (api *Client) SetUserCustomStatus(statusText, statusEmoji string) error {
return api.SetUserCustomStatusContext(context.Background(), statusText, statusEmoji)
// the Slack API will unset the custom status/emoji. If statusExpiration is set to 0
// the status will not expire.
func (api *Client) SetUserCustomStatus(statusText, statusEmoji string, statusExpiration int64) error {
return api.SetUserCustomStatusContext(context.Background(), statusText, statusEmoji, statusExpiration)
}
// SetUserCustomStatusContext will set a custom status and emoji for the currently authenticated user with a custom context
//
// For more information see SetUserCustomStatus
func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, statusEmoji string) error {
func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, statusEmoji string, statusExpiration int64) error {
// XXX(theckman): this anonymous struct is for making requests to the Slack
// API for setting and unsetting a User's Custom Status/Emoji. To change
// these values we must provide a JSON document as the profile POST field.
@ -488,11 +503,13 @@ func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, s
// - https://api.slack.com/docs/presence-and-status#custom_status
profile, err := json.Marshal(
&struct {
StatusText string `json:"status_text"`
StatusEmoji string `json:"status_emoji"`
StatusText string `json:"status_text"`
StatusEmoji string `json:"status_emoji"`
StatusExpiration int64 `json:"status_expiration"`
}{
StatusText: statusText,
StatusEmoji: statusEmoji,
StatusText: statusText,
StatusEmoji: statusEmoji,
StatusExpiration: statusExpiration,
},
)
@ -506,15 +523,11 @@ func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, s
}
response := &userResponseFull{}
if err = postForm(ctx, api.httpclient, SLACK_API+"users.profile.set", values, response, api.debug); err != nil {
if err = api.postMethod(ctx, "users.profile.set", values, response); err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
return response.Err()
}
// UnsetUserCustomStatus removes the custom status message for the currently
@ -526,7 +539,7 @@ func (api *Client) UnsetUserCustomStatus() error {
// UnsetUserCustomStatusContext removes the custom status message for the currently authenticated user
// with a custom context. This is a convenience method that wraps (*Client).SetUserCustomStatus().
func (api *Client) UnsetUserCustomStatusContext(ctx context.Context) error {
return api.SetUserCustomStatusContext(ctx, "", "")
return api.SetUserCustomStatusContext(ctx, "", "", 0)
}
// GetUserProfile retrieves a user's profile information.
@ -547,12 +560,14 @@ func (api *Client) GetUserProfileContext(ctx context.Context, userID string, inc
}
resp := &getUserProfileResponse{}
err := postSlackMethod(ctx, api.httpclient, "users.profile.get", values, &resp, api.debug)
err := api.postMethod(ctx, "users.profile.get", values, &resp)
if err != nil {
return nil, err
}
if !resp.Ok {
return nil, errors.New(resp.Error)
if err := resp.Err(); err != nil {
return nil, err
}
return resp.Profile, nil
}

View File

@ -1,15 +1,22 @@
package slack
import (
"github.com/pkg/errors"
"net/http"
"bytes"
"encoding/json"
"net/http"
"github.com/pkg/errors"
)
type WebhookMessage struct {
Text string `json:"text,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
Username string `json:"username,omitempty"`
IconEmoji string `json:"icon_emoji,omitempty"`
IconURL string `json:"icon_url,omitempty"`
Channel string `json:"channel,omitempty"`
ThreadTimestamp string `json:"thread_ts,omitempty"`
Text string `json:"text,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
Parse string `json:"parse,omitempty"`
}
func PostWebhook(url string, msg *WebhookMessage) error {
@ -19,15 +26,11 @@ func PostWebhook(url string, msg *WebhookMessage) error {
return errors.Wrap(err, "marshal failed")
}
response, err := http.Post(url, "application/json", bytes.NewReader(raw));
response, err := http.Post(url, "application/json", bytes.NewReader(raw))
if err != nil {
return errors.Wrap(err, "failed to post webhook")
}
if response.StatusCode != http.StatusOK {
return statusCodeError{Code: response.StatusCode, Status: response.Status}
}
return nil
return checkStatusCode(response, discard{})
}

View File

@ -2,7 +2,7 @@ package slack
import (
"encoding/json"
"errors"
"net/url"
"sync"
"time"
@ -20,6 +20,9 @@ const (
//
// Create this element with Client's NewRTM() or NewRTMWithOptions(*RTMOptions)
type RTM struct {
// Client is the main API, embedded
Client
idGen IDGenerator
pingInterval time.Duration
pingDeadman *time.Timer
@ -29,15 +32,10 @@ type RTM struct {
IncomingEvents chan RTMEvent
outgoingMessages chan OutgoingMessage
killChannel chan bool
disconnected chan struct{} // disconnected is closed when Disconnect is invoked, regardless of connection state. Allows for ManagedConnection to not leak.
disconnected chan struct{}
disconnectedm *sync.Once
forcePing chan bool
rawEvents chan json.RawMessage
wasIntentional bool
isConnected bool
// Client is the main API, embedded
Client
websocketURL string
// UserDetails upon connection
info *Info
@ -53,40 +51,30 @@ type RTM struct {
// mu is mutex used to prevent RTM connection race conditions
mu *sync.Mutex
// connParams is a map of flags for connection parameters.
connParams url.Values
}
// RTMOptions allows configuration of various options available for RTM messaging
//
// This structure will evolve in time so please make sure you are always using the
// named keys for every entry available as per Go 1 compatibility promise adding fields
// to this structure should not be considered a breaking change.
type RTMOptions struct {
// UseRTMStart set to true in order to use rtm.start or false to use rtm.connect
// As of 11th July 2017 you should prefer setting this to false, see:
// https://api.slack.com/changelog/2017-04-start-using-rtm-connect-and-stop-using-rtm-start
UseRTMStart bool
// signal that we are disconnected by closing the channel.
// protect it with a mutex to ensure it only happens once.
func (rtm *RTM) disconnect() {
rtm.disconnectedm.Do(func() {
close(rtm.disconnected)
})
}
// Disconnect and wait, blocking until a successful disconnection.
func (rtm *RTM) Disconnect() error {
// avoid RTM disconnect race conditions
rtm.mu.Lock()
defer rtm.mu.Unlock()
// always push into the disconnected channel when invoked,
// always push into the kill channel when invoked,
// this lets the ManagedConnection() function properly clean up.
// if the buffer is full then just continue on.
select {
case rtm.disconnected <- struct{}{}:
default:
case rtm.killChannel <- true:
return nil
case <-rtm.disconnected:
return ErrAlreadyDisconnected
}
if !rtm.isConnected {
return errors.New("Invalid call to Disconnect - Slack API is already disconnected")
}
rtm.killChannel <- true
return nil
}
// GetInfo returns the info structure received when calling
@ -110,7 +98,7 @@ func (rtm *RTM) SendMessage(msg *OutgoingMessage) {
}
func (rtm *RTM) resetDeadman() {
timerReset(rtm.pingDeadman, deadmanDuration(rtm.pingInterval))
rtm.pingDeadman.Reset(deadmanDuration(rtm.pingInterval))
}
func deadmanDuration(d time.Duration) time.Duration {

View File

@ -18,6 +18,7 @@ type ConnectedEvent struct {
// ConnectionErrorEvent contains information about a connection error
type ConnectionErrorEvent struct {
Attempt int
Backoff time.Duration // how long we'll wait before the next attempt
ErrorObj error
}

View File

@ -5,10 +5,12 @@ import (
"fmt"
"io"
"net/http"
stdurl "net/url"
"reflect"
"time"
"github.com/gorilla/websocket"
"github.com/nlopes/slack/internal/timex"
)
// ManageConnection can be called on a Slack RTM instance returned by the
@ -37,6 +39,7 @@ func (rtm *RTM) ManageConnection() {
if info, conn, err = rtm.connect(connectionCount, rtm.useRTMStart); err != nil {
// when the connection is unsuccessful its fatal, and we need to bail out.
rtm.Debugf("Failed to connect with RTM on try %d: %s", connectionCount, err)
rtm.disconnect()
return
}
@ -44,7 +47,6 @@ func (rtm *RTM) ManageConnection() {
// and conn.
rtm.mu.Lock()
rtm.conn = conn
rtm.isConnected = true
rtm.info = info
rtm.mu.Unlock()
@ -55,20 +57,19 @@ func (rtm *RTM) ManageConnection() {
rtm.Debugf("RTM connection succeeded on try %d", connectionCount)
keepRunning := make(chan bool)
// we're now connected (or have failed fatally) so we can set up
// listeners
go rtm.handleIncomingEvents(keepRunning)
// we're now connected so we can set up listeners
go rtm.handleIncomingEvents()
// this should be a blocking call until the connection has ended
rtm.handleEvents(keepRunning)
rtm.handleEvents()
// after being disconnected we need to check if it was intentional
// if not then we should try to reconnect
if rtm.wasIntentional {
select {
case <-rtm.disconnected:
// after handle events returns we need to check if we're disconnected
return
default:
// otherwise continue and run the loop again to reconnect
}
// else continue and run the loop again to connect
}
}
@ -87,18 +88,20 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke
// used to provide exponential backoff wait time with jitter before trying
// to connect to slack again
boff := &backoff{
Min: 100 * time.Millisecond,
Max: 5 * time.Minute,
Factor: 2,
Jitter: true,
Max: 5 * time.Minute,
}
for {
var (
backoff time.Duration
)
// send connecting event
rtm.IncomingEvents <- RTMEvent{"connecting", &ConnectingEvent{
Attempt: boff.attempts + 1,
ConnectionCount: connectionCount,
}}
// attempt to start the connection
info, conn, err := rtm.startRTMAndDial(useRTMStart)
if err == nil {
@ -108,32 +111,48 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke
// check for fatal errors
switch err.Error() {
case errInvalidAuth, errInactiveAccount, errMissingAuthToken:
rtm.Debugf("Invalid auth when connecting with RTM: %s", err)
rtm.Debugf("invalid auth when connecting with RTM: %s", err)
rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}}
return nil, nil, err
default:
}
switch actual := err.(type) {
case statusCodeError:
if actual.Code == http.StatusNotFound {
rtm.Debugf("invalid auth when connecting with RTM: %s", err)
rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}}
return nil, nil, err
}
case *RateLimitedError:
backoff = actual.RetryAfter
default:
}
backoff = timex.Max(backoff, boff.Duration())
// any other errors are treated as recoverable and we try again after
// sending the event along the IncomingEvents channel
rtm.IncomingEvents <- RTMEvent{"connection_error", &ConnectionErrorEvent{
Attempt: boff.attempts,
Backoff: backoff,
ErrorObj: err,
}}
// check if Disconnect() has been invoked.
select {
case intentional := <-rtm.killChannel:
if intentional {
rtm.killConnection(intentional)
return nil, nil, ErrRTMDisconnected
}
case <-rtm.disconnected:
rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: true}}
return nil, nil, fmt.Errorf("disconnect received while trying to connect")
return nil, nil, ErrRTMDisconnected
default:
}
// get time we should wait before attempting to connect again
dur := boff.Duration()
rtm.Debugf("reconnection %d failed: %s", boff.attempts+1, err)
rtm.Debugln(" -> reconnecting in", dur)
time.Sleep(dur)
rtm.Debugf("reconnection %d failed: %s reconnecting in %v\n", boff.attempts, err, backoff)
time.Sleep(backoff)
}
}
@ -157,6 +176,14 @@ func (rtm *RTM) startRTMAndDial(useRTMStart bool) (info *Info, _ *websocket.Conn
return nil, nil, err
}
// install connection parameters
u, err := stdurl.Parse(url)
if err != nil {
return nil, nil, err
}
u.RawQuery = rtm.connParams.Encode()
url = u.String()
rtm.Debugf("Dialing to websocket on url %s", url)
// Only use HTTPS for connections to prevent MITM attacks on the connection.
upgradeHeader := http.Header{}
@ -178,15 +205,19 @@ func (rtm *RTM) startRTMAndDial(useRTMStart bool) (info *Info, _ *websocket.Conn
//
// This should not be called directly! Instead a boolean value (true for
// intentional, false otherwise) should be sent to the killChannel on the RTM.
func (rtm *RTM) killConnection(keepRunning chan bool, intentional bool) error {
func (rtm *RTM) killConnection(intentional bool) (err error) {
rtm.Debugln("killing connection")
if rtm.isConnected {
close(keepRunning)
if rtm.conn != nil {
err = rtm.conn.Close()
}
rtm.isConnected = false
rtm.wasIntentional = intentional
err := rtm.conn.Close()
rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{intentional}}
if intentional {
rtm.disconnect()
}
return err
}
@ -195,31 +226,29 @@ func (rtm *RTM) killConnection(keepRunning chan bool, intentional bool) error {
// interval. This also sends outgoing messages that are received from the RTM's
// outgoingMessages channel. This also handles incoming raw events from the RTM
// rawEvents channel.
func (rtm *RTM) handleEvents(keepRunning chan bool) {
func (rtm *RTM) handleEvents() {
ticker := time.NewTicker(rtm.pingInterval)
defer ticker.Stop()
for {
select {
// catch "stop" signal on channel close
case intentional := <-rtm.killChannel:
_ = rtm.killConnection(keepRunning, intentional)
_ = rtm.killConnection(intentional)
return
// detect when the connection is dead.
case <-rtm.pingDeadman.C:
rtm.Debugln("deadman switch trigger disconnecting")
_ = rtm.killConnection(keepRunning, false)
_ = rtm.killConnection(false)
return
// send pings on ticker interval
case <-ticker.C:
err := rtm.ping()
if err != nil {
_ = rtm.killConnection(keepRunning, false)
if err := rtm.ping(); err != nil {
_ = rtm.killConnection(false)
return
}
case <-rtm.forcePing:
err := rtm.ping()
if err != nil {
_ = rtm.killConnection(keepRunning, false)
if err := rtm.ping(); err != nil {
_ = rtm.killConnection(false)
return
}
// listen for messages that need to be sent
@ -229,7 +258,8 @@ func (rtm *RTM) handleEvents(keepRunning chan bool) {
case rawEvent := <-rtm.rawEvents:
switch rtm.handleRawEvent(rawEvent) {
case rtmEventTypeGoodbye:
_ = rtm.killConnection(keepRunning, false)
_ = rtm.killConnection(false)
return
default:
}
}
@ -241,17 +271,10 @@ func (rtm *RTM) handleEvents(keepRunning chan bool) {
//
// This will stop executing once the RTM's keepRunning channel has been closed
// or has anything sent to it.
func (rtm *RTM) handleIncomingEvents(keepRunning <-chan bool) {
func (rtm *RTM) handleIncomingEvents() {
for {
// non-blocking listen to see if channel is closed
select {
// catch "stop" signal on channel close
case <-keepRunning:
if err := rtm.receiveIncomingEvent(); err != nil {
return
default:
if err := rtm.receiveIncomingEvent(); err != nil {
return
}
}
}
}
@ -274,7 +297,7 @@ func (rtm *RTM) sendWithDeadline(msg interface{}) error {
// and instead lets a future failed 'PING' detect the failed connection.
func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) {
rtm.Debugln("Sending message:", msg)
if len(msg.Text) > MaxMessageTextLength {
if len([]rune(msg.Text)) > MaxMessageTextLength {
rtm.IncomingEvents <- RTMEvent{"outgoing_error", &MessageTooLongEvent{
Message: msg,
MaxLength: MaxMessageTextLength,
@ -323,20 +346,32 @@ func (rtm *RTM) receiveIncomingEvent() error {
// 'PING' message
// trigger a 'PING' to detect potential websocket disconnect
rtm.forcePing <- true
select {
case rtm.forcePing <- true:
case <-rtm.disconnected:
}
case err != nil:
// All other errors from ReadJSON come from NextReader, and should
// kill the read loop and force a reconnect.
rtm.IncomingEvents <- RTMEvent{"incoming_error", &IncomingEventError{
ErrorObj: err,
}}
rtm.killChannel <- false
select {
case rtm.killChannel <- false:
case <-rtm.disconnected:
}
return err
case len(event) == 0:
rtm.Debugln("Received empty event")
default:
rtm.Debugln("Incoming Event:", string(event[:]))
rtm.rawEvents <- event
rtm.Debugln("Incoming Event:", string(event))
select {
case rtm.rawEvents <- event:
case <-rtm.disconnected:
rtm.Debugln("disonnected while attempting to send raw event")
}
}
return nil
}
@ -405,8 +440,7 @@ func (rtm *RTM) handlePong(event json.RawMessage) {
rtm.resetDeadman()
if err := json.Unmarshal(event, &p); err != nil {
logger.Println("RTM Error unmarshalling 'pong' event:", err)
rtm.Debugln(" -> Erroneous 'ping' event:", string(event))
rtm.Client.log.Println("RTM Error unmarshalling 'pong' event:", err)
return
}
@ -423,8 +457,8 @@ func (rtm *RTM) handlePong(event json.RawMessage) {
func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) {
v, exists := EventMapping[typeStr]
if !exists {
rtm.Debugf("RTM Error, received unmapped event %q: %s\n", typeStr, string(event))
err := fmt.Errorf("RTM Error: Received unmapped event %q: %s\n", typeStr, string(event))
rtm.Debugf("RTM Error - received unmapped event %q: %s\n", typeStr, string(event))
err := fmt.Errorf("RTM Error: Received unmapped event %q: %s", typeStr, string(event))
rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}}
return
}
@ -433,7 +467,7 @@ func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) {
err := json.Unmarshal(event, recvEvent)
if err != nil {
rtm.Debugf("RTM Error, could not unmarshall event %q: %s\n", typeStr, string(event))
err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s\n", typeStr, string(event))
err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s", typeStr, string(event))
rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}}
return
}
@ -524,4 +558,9 @@ var EventMapping = map[string]interface{}{
"member_joined_channel": MemberJoinedChannelEvent{},
"member_left_channel": MemberLeftChannelEvent{},
"subteam_created": SubteamCreatedEvent{},
"subteam_self_added": SubteamSelfAddedEvent{},
"subteam_self_removed": SubteamSelfRemovedEvent{},
"subteam_updated": SubteamUpdatedEvent{},
}

View File

@ -43,9 +43,10 @@ type HelloEvent struct{}
// PresenceChangeEvent represents the presence change event
type PresenceChangeEvent struct {
Type string `json:"type"`
Presence string `json:"presence"`
User string `json:"user"`
Type string `json:"type"`
Presence string `json:"presence"`
User string `json:"user"`
Users []string `json:"users"`
}
// UserTypingEvent represents the user typing event

35
vendor/github.com/nlopes/slack/websocket_subteam.go generated vendored Normal file
View File

@ -0,0 +1,35 @@
package slack
// SubteamCreatedEvent represents the Subteam created event
type SubteamCreatedEvent struct {
Type string `json:"type"`
Subteam UserGroup `json:"subteam"`
}
// SubteamCreatedEvent represents the membership of an existing User Group has changed event
type SubteamMembersChangedEvent struct {
Type string `json:"type"`
SubteamID string `json:"subteam_id"`
TeamID string `json:"team_id"`
DatePreviousUpdate JSONTime `json:"date_previous_update"`
DateUpdate JSONTime `json:"date_update"`
AddedUsers []string `json:"added_users"`
AddedUsersCount string `json:"added_users_count"`
RemovedUsers []string `json:"removed_users"`
RemovedUsersCount string `json:"removed_users_count"`
}
// SubteamSelfAddedEvent represents an event of you have been added to a User Group
type SubteamSelfAddedEvent struct {
Type string `json:"type"`
SubteamID string `json:"subteam_id"`
}
// SubteamSelfRemovedEvent represents an event of you have been removed from a User Group
type SubteamSelfRemovedEvent SubteamSelfAddedEvent
// SubteamUpdatedEvent represents an event of an existing User Group has been updated or its members changed
type SubteamUpdatedEvent struct {
Type string `json:"type"`
Subteam UserGroup `json:"subteam"`
}

0
vendor/github.com/nsf/termbox-go/collect_terminfo.py generated vendored Executable file → Normal file
View File

View File

@ -1,10 +1,14 @@
language: go
go_import_path: github.com/pkg/errors
go:
- 1.4.3
- 1.5.4
- 1.6.2
- 1.7.1
- 1.4.x
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- 1.11.x
- tip
script:

View File

@ -1,4 +1,4 @@
# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors)
# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) [![Sourcegraph](https://sourcegraph.com/github.com/pkg/errors/-/badge.svg)](https://sourcegraph.com/github.com/pkg/errors?badge)
Package errors provides simple error handling primitives.
@ -47,6 +47,6 @@ We welcome pull requests, bug fixes and issue reports. With that said, the bar f
Before proposing a change, please discuss your change by raising an issue.
## Licence
## License
BSD-2-Clause

View File

@ -6,7 +6,7 @@
// return err
// }
//
// which applied recursively up the call stack results in error reports
// which when applied recursively up the call stack results in error reports
// without context or debugging information. The errors package allows
// programmers to add context to the failure path in their code in a way
// that does not destroy the original value of the error.
@ -15,16 +15,17 @@
//
// The errors.Wrap function returns a new error that adds context to the
// original error by recording a stack trace at the point Wrap is called,
// and the supplied message. For example
// together with the supplied message. For example
//
// _, err := ioutil.ReadAll(r)
// if err != nil {
// return errors.Wrap(err, "read failed")
// }
//
// If additional control is required the errors.WithStack and errors.WithMessage
// functions destructure errors.Wrap into its component operations of annotating
// an error with a stack trace and an a message, respectively.
// If additional control is required, the errors.WithStack and
// errors.WithMessage functions destructure errors.Wrap into its component
// operations: annotating an error with a stack trace and with a message,
// respectively.
//
// Retrieving the cause of an error
//
@ -38,7 +39,7 @@
// }
//
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
// the topmost error which does not implement causer, which is assumed to be
// the topmost error that does not implement causer, which is assumed to be
// the original cause. For example:
//
// switch err := errors.Cause(err).(type) {
@ -48,16 +49,16 @@
// // unknown error
// }
//
// causer interface is not exported by this package, but is considered a part
// of stable public API.
// Although the causer interface is not exported by this package, it is
// considered a part of its stable public interface.
//
// Formatted printing of errors
//
// All error values returned from this package implement fmt.Formatter and can
// be formatted by the fmt package. The following verbs are supported
// be formatted by the fmt package. The following verbs are supported:
//
// %s print the error. If the error has a Cause it will be
// printed recursively
// printed recursively.
// %v see %s
// %+v extended format. Each Frame of the error's StackTrace will
// be printed in detail.
@ -65,13 +66,13 @@
// Retrieving the stack trace of an error or wrapper
//
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
// invoked. This information can be retrieved with the following interface.
// invoked. This information can be retrieved with the following interface:
//
// type stackTracer interface {
// StackTrace() errors.StackTrace
// }
//
// Where errors.StackTrace is defined as
// The returned errors.StackTrace type is defined as
//
// type StackTrace []Frame
//
@ -85,8 +86,8 @@
// }
// }
//
// stackTracer interface is not exported by this package, but is considered a part
// of stable public API.
// Although the stackTracer interface is not exported by this package, it is
// considered a part of its stable public interface.
//
// See the documentation for Frame.Format for more details.
package errors
@ -192,7 +193,7 @@ func Wrap(err error, message string) error {
}
// Wrapf returns an error annotating err with a stack trace
// at the point Wrapf is call, and the format specifier.
// at the point Wrapf is called, and the format specifier.
// If err is nil, Wrapf returns nil.
func Wrapf(err error, format string, args ...interface{}) error {
if err == nil {
@ -220,6 +221,18 @@ func WithMessage(err error, message string) error {
}
}
// WithMessagef annotates err with the format specifier.
// If err is nil, WithMessagef returns nil.
func WithMessagef(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
return &withMessage{
cause: err,
msg: fmt.Sprintf(format, args...),
}
}
type withMessage struct {
cause error
msg string

View File

@ -46,7 +46,8 @@ func (f Frame) line() int {
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
// %+s path of source file relative to the compile time GOPATH
// %+s function name and path of source file relative to the compile time
// GOPATH separated by \n\t (<funcname>\n\t<path>)
// %+v equivalent to %+s:%d
func (f Frame) Format(s fmt.State, verb rune) {
switch verb {
@ -79,6 +80,14 @@ func (f Frame) Format(s fmt.State, verb rune) {
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
type StackTrace []Frame
// Format formats the stack of Frames according to the fmt.Formatter interface.
//
// %s lists source files for each Frame in the stack
// %v lists the source file and line number for each Frame in the stack
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
// %+v Prints filename, function, and line number for each Frame in the stack.
func (st StackTrace) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
@ -136,43 +145,3 @@ func funcname(name string) string {
i = strings.Index(name, ".")
return name[i+1:]
}
func trimGOPATH(name, file string) string {
// Here we want to get the source file path relative to the compile time
// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
// GOPATH at runtime, but we can infer the number of path segments in the
// GOPATH. We note that fn.Name() returns the function name qualified by
// the import path, which does not include the GOPATH. Thus we can trim
// segments from the beginning of the file path until the number of path
// separators remaining is one more than the number of path separators in
// the function name. For example, given:
//
// GOPATH /home/user
// file /home/user/src/pkg/sub/file.go
// fn.Name() pkg/sub.Type.Method
//
// We want to produce:
//
// pkg/sub/file.go
//
// From this we can easily see that fn.Name() has one less path separator
// than our desired output. We count separators from the end of the file
// path until it finds two more than in the function name and then move
// one character forward to preserve the initial path segment without a
// leading separator.
const sep = "/"
goal := strings.Count(name, sep) + 2
i := len(file)
for n := 0; n < goal; n++ {
i = strings.LastIndex(file[:i], sep)
if i == -1 {
// not enough separators found, set i so that the slice expression
// below leaves file unmodified
i = -len(sep)
break
}
}
// get back to 0 or trim the leading separator
file = file[i+len(sep):]
return file
}

23
vendor/modules.txt vendored Normal file
View File

@ -0,0 +1,23 @@
# github.com/0xAX/notificator v0.0.0-20171022182052-88d57ee9043b
github.com/0xAX/notificator
# github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04
github.com/erroneousboat/termui
# github.com/gorilla/websocket v1.4.0
github.com/gorilla/websocket
# github.com/maruel/panicparse v1.1.1
github.com/maruel/panicparse/stack
# github.com/mattn/go-runewidth v0.0.3
github.com/mattn/go-runewidth
# github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7
github.com/mitchellh/go-wordwrap
# github.com/nlopes/slack v0.5.1-0.20190515005541-e2954b1409b0
github.com/nlopes/slack
github.com/nlopes/slack/internal/errorsx
github.com/nlopes/slack/internal/timex
github.com/nlopes/slack/slackutilsx
# github.com/nsf/termbox-go v0.0.0-20180819125858-b66b20ab708e
github.com/nsf/termbox-go
# github.com/pkg/errors v0.8.1
github.com/pkg/errors
# github.com/renstrom/fuzzysearch v1.0.1
github.com/renstrom/fuzzysearch/fuzzy