Update vendor folder

This commit is contained in:
erroneousboat 2019-10-05 12:01:51 +02:00
parent 8ec19e2db2
commit 240deb0607
38 changed files with 688 additions and 245 deletions

9
go.mod
View File

@ -5,13 +5,16 @@ go 1.12
require ( require (
github.com/0xAX/notificator v0.0.0-20171022182052-88d57ee9043b github.com/0xAX/notificator v0.0.0-20171022182052-88d57ee9043b
github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04 github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04
github.com/gorilla/websocket v1.4.0 // indirect github.com/gorilla/websocket v1.4.1 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/maruel/panicparse v1.1.1 // indirect github.com/maruel/panicparse v1.1.1 // indirect
github.com/mattn/go-runewidth v0.0.3 github.com/mattn/go-runewidth v0.0.3
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/nlopes/slack v0.5.1-0.20190515005541-e2954b1409b0 github.com/nlopes/slack v0.6.0
github.com/nsf/termbox-go v0.0.0-20180819125858-b66b20ab708e github.com/nsf/termbox-go v0.0.0-20180819125858-b66b20ab708e
github.com/pkg/errors v0.8.1 // indirect github.com/pkg/errors v0.8.1 // indirect
github.com/renstrom/fuzzysearch v1.0.1 github.com/renstrom/fuzzysearch v1.0.1
github.com/stretchr/testify v1.3.0 // indirect github.com/stretchr/testify v1.4.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.2.4 // indirect
) )

24
go.sum
View File

@ -1,28 +1,42 @@
github.com/0xAX/notificator v0.0.0-20171022182052-88d57ee9043b h1:Sn+u6zpXFyfm2X7ruh+z6SJiUVyFg8YElh6HIOhrRCA= 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/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.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/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 h1:DaFwoQC0Neeb2y2CVFxDPrS1BeyWAkKc4VVBDTZ0N98=
github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04/go.mod h1:UPpsbgDrqmUayOFOkCGD7+xrBIml/1dA0dsqTRnZqac= 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.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/maruel/panicparse v1.1.1 h1:k62YPcEoLncEEpjMt92GtG5ugb8WL/510Ys3/h5IkRc= 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/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 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 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 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 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.6.0 h1:jt0jxVQGhssx1Ib7naAOZEZcGdtIhTzkP0nopK0AsRA=
github.com/nlopes/slack v0.5.1-0.20190515005541-e2954b1409b0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= github.com/nlopes/slack v0.6.0/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 h1:fvw0uluMptljaRKSU8459cJ4bmi3qUYyMs5kzpic2fY=
github.com/nsf/termbox-go v0.0.0-20180819125858-b66b20ab708e/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= 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.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 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/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/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 h1:hnh2Fhqqa5I41Xgmm7UMAYgEIRn/iZwWItfwUHr1IWE=
github.com/renstrom/fuzzysearch v1.0.1/go.mod h1:SAEjPB4voP88qmWJXI7mA5m15uNlEnuHLx4Eu2mPGpQ= 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/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,19 +0,0 @@
language: go
sudo: false
matrix:
include:
- 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
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d .)
- go vet $(go list ./... | grep -v /vendor/)
- go test -v -race ./...

View File

@ -1,11 +1,11 @@
# Gorilla WebSocket # Gorilla WebSocket
[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket)
[![CircleCI](https://circleci.com/gh/gorilla/websocket.svg?style=svg)](https://circleci.com/gh/gorilla/websocket)
Gorilla WebSocket is a [Go](http://golang.org/) implementation of the Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
[![Build Status](https://travis-ci.org/gorilla/websocket.svg?branch=master)](https://travis-ci.org/gorilla/websocket)
[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket)
### Documentation ### Documentation
* [API Reference](http://godoc.org/github.com/gorilla/websocket) * [API Reference](http://godoc.org/github.com/gorilla/websocket)
@ -27,7 +27,7 @@ package API is stable.
### Protocol Compliance ### Protocol Compliance
The Gorilla WebSocket package passes the server tests in the [Autobahn Test The Gorilla WebSocket package passes the server tests in the [Autobahn Test
Suite](http://autobahn.ws/testsuite) using the application in the [examples/autobahn Suite](https://github.com/crossbario/autobahn-testsuite) using the application in the [examples/autobahn
subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn). subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn).
### Gorilla WebSocket compared with other packages ### Gorilla WebSocket compared with other packages
@ -40,7 +40,7 @@ subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn
</tr> </tr>
<tr> <tr>
<tr><td colspan="3"><a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a> Features</td></tr> <tr><td colspan="3"><a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a> Features</td></tr>
<tr><td>Passes <a href="http://autobahn.ws/testsuite/">Autobahn Test Suite</a></td><td><a href="https://github.com/gorilla/websocket/tree/master/examples/autobahn">Yes</a></td><td>No</td></tr> <tr><td>Passes <a href="https://github.com/crossbario/autobahn-testsuite">Autobahn Test Suite</a></td><td><a href="https://github.com/gorilla/websocket/tree/master/examples/autobahn">Yes</a></td><td>No</td></tr>
<tr><td>Receive <a href="https://tools.ietf.org/html/rfc6455#section-5.4">fragmented</a> message<td>Yes</td><td><a href="https://code.google.com/p/go/issues/detail?id=7632">No</a>, see note 1</td></tr> <tr><td>Receive <a href="https://tools.ietf.org/html/rfc6455#section-5.4">fragmented</a> message<td>Yes</td><td><a href="https://code.google.com/p/go/issues/detail?id=7632">No</a>, see note 1</td></tr>
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.1">close</a> message</td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td><a href="https://code.google.com/p/go/issues/detail?id=4588">No</a></td></tr> <tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.1">close</a> message</td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td><a href="https://code.google.com/p/go/issues/detail?id=4588">No</a></td></tr>
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.2">pings</a> and receive <a href="https://tools.ietf.org/html/rfc6455#section-5.5.3">pongs</a></td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td>No</td></tr> <tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.2">pings</a> and receive <a href="https://tools.ietf.org/html/rfc6455#section-5.5.3">pongs</a></td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td>No</td></tr>

View File

@ -70,7 +70,7 @@ type Dialer struct {
// HandshakeTimeout specifies the duration for the handshake to complete. // HandshakeTimeout specifies the duration for the handshake to complete.
HandshakeTimeout time.Duration HandshakeTimeout time.Duration
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer // ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer
// size is zero, then a useful default size is used. The I/O buffer sizes // size is zero, then a useful default size is used. The I/O buffer sizes
// do not limit the size of the messages that can be sent or received. // do not limit the size of the messages that can be sent or received.
ReadBufferSize, WriteBufferSize int ReadBufferSize, WriteBufferSize int
@ -140,7 +140,7 @@ var nilDialer = *DefaultDialer
// Use the response.Header to get the selected subprotocol // Use the response.Header to get the selected subprotocol
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie). // (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
// //
// The context will be used in the request and in the Dialer // The context will be used in the request and in the Dialer.
// //
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a // If the WebSocket handshake fails, ErrBadHandshake is returned along with a
// non-nil *http.Response so that callers can handle redirects, authentication, // non-nil *http.Response so that callers can handle redirects, authentication,

View File

@ -260,10 +260,12 @@ type Conn struct {
newCompressionWriter func(io.WriteCloser, int) io.WriteCloser newCompressionWriter func(io.WriteCloser, int) io.WriteCloser
// Read fields // Read fields
reader io.ReadCloser // the current reader returned to the application reader io.ReadCloser // the current reader returned to the application
readErr error readErr error
br *bufio.Reader br *bufio.Reader
readRemaining int64 // bytes remaining in current frame. // bytes remaining in current frame.
// set setReadRemaining to safely update this value and prevent overflow
readRemaining int64
readFinal bool // true the current message has more frames. readFinal bool // true the current message has more frames.
readLength int64 // Message size. readLength int64 // Message size.
readLimit int64 // Maximum message size. readLimit int64 // Maximum message size.
@ -320,6 +322,17 @@ func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int,
return c return c
} }
// setReadRemaining tracks the number of bytes remaining on the connection. If n
// overflows, an ErrReadLimit is returned.
func (c *Conn) setReadRemaining(n int64) error {
if n < 0 {
return ErrReadLimit
}
c.readRemaining = n
return nil
}
// Subprotocol returns the negotiated protocol for the connection. // Subprotocol returns the negotiated protocol for the connection.
func (c *Conn) Subprotocol() string { func (c *Conn) Subprotocol() string {
return c.subprotocol return c.subprotocol
@ -451,7 +464,8 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er
return err return err
} }
func (c *Conn) prepWrite(messageType int) error { // beginMessage prepares a connection and message writer for a new message.
func (c *Conn) beginMessage(mw *messageWriter, messageType int) error {
// Close previous writer if not already closed by the application. It's // Close previous writer if not already closed by the application. It's
// probably better to return an error in this situation, but we cannot // probably better to return an error in this situation, but we cannot
// change this without breaking existing applications. // change this without breaking existing applications.
@ -471,6 +485,10 @@ func (c *Conn) prepWrite(messageType int) error {
return err return err
} }
mw.c = c
mw.frameType = messageType
mw.pos = maxFrameHeaderSize
if c.writeBuf == nil { if c.writeBuf == nil {
wpd, ok := c.writePool.Get().(writePoolData) wpd, ok := c.writePool.Get().(writePoolData)
if ok { if ok {
@ -491,16 +509,11 @@ func (c *Conn) prepWrite(messageType int) error {
// All message types (TextMessage, BinaryMessage, CloseMessage, PingMessage and // All message types (TextMessage, BinaryMessage, CloseMessage, PingMessage and
// PongMessage) are supported. // PongMessage) are supported.
func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) { func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {
if err := c.prepWrite(messageType); err != nil { var mw messageWriter
if err := c.beginMessage(&mw, messageType); err != nil {
return nil, err return nil, err
} }
c.writer = &mw
mw := &messageWriter{
c: c,
frameType: messageType,
pos: maxFrameHeaderSize,
}
c.writer = mw
if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) { if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) {
w := c.newCompressionWriter(c.writer, c.compressionLevel) w := c.newCompressionWriter(c.writer, c.compressionLevel)
mw.compress = true mw.compress = true
@ -517,10 +530,16 @@ type messageWriter struct {
err error err error
} }
func (w *messageWriter) fatal(err error) error { func (w *messageWriter) endMessage(err error) error {
if w.err != nil { if w.err != nil {
w.err = err return err
w.c.writer = nil }
c := w.c
w.err = err
c.writer = nil
if c.writePool != nil {
c.writePool.Put(writePoolData{buf: c.writeBuf})
c.writeBuf = nil
} }
return err return err
} }
@ -534,7 +553,7 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error {
// Check for invalid control frames. // Check for invalid control frames.
if isControl(w.frameType) && if isControl(w.frameType) &&
(!final || length > maxControlFramePayloadSize) { (!final || length > maxControlFramePayloadSize) {
return w.fatal(errInvalidControlFrame) return w.endMessage(errInvalidControlFrame)
} }
b0 := byte(w.frameType) b0 := byte(w.frameType)
@ -579,7 +598,7 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error {
copy(c.writeBuf[maxFrameHeaderSize-4:], key[:]) copy(c.writeBuf[maxFrameHeaderSize-4:], key[:])
maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:w.pos]) maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:w.pos])
if len(extra) > 0 { if len(extra) > 0 {
return c.writeFatal(errors.New("websocket: internal error, extra used in client mode")) return w.endMessage(c.writeFatal(errors.New("websocket: internal error, extra used in client mode")))
} }
} }
@ -600,15 +619,11 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error {
c.isWriting = false c.isWriting = false
if err != nil { if err != nil {
return w.fatal(err) return w.endMessage(err)
} }
if final { if final {
c.writer = nil w.endMessage(errWriteClosed)
if c.writePool != nil {
c.writePool.Put(writePoolData{buf: c.writeBuf})
c.writeBuf = nil
}
return nil return nil
} }
@ -706,11 +721,7 @@ func (w *messageWriter) Close() error {
if w.err != nil { if w.err != nil {
return w.err return w.err
} }
if err := w.flushFrame(true, nil); err != nil { return w.flushFrame(true, nil)
return err
}
w.err = errWriteClosed
return nil
} }
// WritePreparedMessage writes prepared message into connection. // WritePreparedMessage writes prepared message into connection.
@ -742,10 +753,10 @@ func (c *Conn) WriteMessage(messageType int, data []byte) error {
if c.isServer && (c.newCompressionWriter == nil || !c.enableWriteCompression) { if c.isServer && (c.newCompressionWriter == nil || !c.enableWriteCompression) {
// Fast path with no allocations and single frame. // Fast path with no allocations and single frame.
if err := c.prepWrite(messageType); err != nil { var mw messageWriter
if err := c.beginMessage(&mw, messageType); err != nil {
return err return err
} }
mw := messageWriter{c: c, frameType: messageType, pos: maxFrameHeaderSize}
n := copy(c.writeBuf[mw.pos:], data) n := copy(c.writeBuf[mw.pos:], data)
mw.pos += n mw.pos += n
data = data[n:] data = data[n:]
@ -792,7 +803,7 @@ func (c *Conn) advanceFrame() (int, error) {
final := p[0]&finalBit != 0 final := p[0]&finalBit != 0
frameType := int(p[0] & 0xf) frameType := int(p[0] & 0xf)
mask := p[1]&maskBit != 0 mask := p[1]&maskBit != 0
c.readRemaining = int64(p[1] & 0x7f) c.setReadRemaining(int64(p[1] & 0x7f))
c.readDecompress = false c.readDecompress = false
if c.newDecompressionReader != nil && (p[0]&rsv1Bit) != 0 { if c.newDecompressionReader != nil && (p[0]&rsv1Bit) != 0 {
@ -826,7 +837,17 @@ func (c *Conn) advanceFrame() (int, error) {
return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType)) return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType))
} }
// 3. Read and parse frame length. // 3. Read and parse frame length as per
// https://tools.ietf.org/html/rfc6455#section-5.2
//
// The length of the "Payload data", in bytes: if 0-125, that is the payload
// length.
// - If 126, the following 2 bytes interpreted as a 16-bit unsigned
// integer are the payload length.
// - If 127, the following 8 bytes interpreted as
// a 64-bit unsigned integer (the most significant bit MUST be 0) are the
// payload length. Multibyte length quantities are expressed in network byte
// order.
switch c.readRemaining { switch c.readRemaining {
case 126: case 126:
@ -834,13 +855,19 @@ func (c *Conn) advanceFrame() (int, error) {
if err != nil { if err != nil {
return noFrame, err return noFrame, err
} }
c.readRemaining = int64(binary.BigEndian.Uint16(p))
if err := c.setReadRemaining(int64(binary.BigEndian.Uint16(p))); err != nil {
return noFrame, err
}
case 127: case 127:
p, err := c.read(8) p, err := c.read(8)
if err != nil { if err != nil {
return noFrame, err return noFrame, err
} }
c.readRemaining = int64(binary.BigEndian.Uint64(p))
if err := c.setReadRemaining(int64(binary.BigEndian.Uint64(p))); err != nil {
return noFrame, err
}
} }
// 4. Handle frame masking. // 4. Handle frame masking.
@ -863,6 +890,12 @@ func (c *Conn) advanceFrame() (int, error) {
if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage { if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage {
c.readLength += c.readRemaining c.readLength += c.readRemaining
// Don't allow readLength to overflow in the presence of a large readRemaining
// counter.
if c.readLength < 0 {
return noFrame, ErrReadLimit
}
if c.readLimit > 0 && c.readLength > c.readLimit { if c.readLimit > 0 && c.readLength > c.readLimit {
c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait)) c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait))
return noFrame, ErrReadLimit return noFrame, ErrReadLimit
@ -876,7 +909,7 @@ func (c *Conn) advanceFrame() (int, error) {
var payload []byte var payload []byte
if c.readRemaining > 0 { if c.readRemaining > 0 {
payload, err = c.read(int(c.readRemaining)) payload, err = c.read(int(c.readRemaining))
c.readRemaining = 0 c.setReadRemaining(0)
if err != nil { if err != nil {
return noFrame, err return noFrame, err
} }
@ -949,6 +982,7 @@ func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
c.readErr = hideTempErr(err) c.readErr = hideTempErr(err)
break break
} }
if frameType == TextMessage || frameType == BinaryMessage { if frameType == TextMessage || frameType == BinaryMessage {
c.messageReader = &messageReader{c} c.messageReader = &messageReader{c}
c.reader = c.messageReader c.reader = c.messageReader
@ -989,7 +1023,9 @@ func (r *messageReader) Read(b []byte) (int, error) {
if c.isServer { if c.isServer {
c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n]) c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n])
} }
c.readRemaining -= int64(n) rem := c.readRemaining
rem -= int64(n)
c.setReadRemaining(rem)
if c.readRemaining > 0 && c.readErr == io.EOF { if c.readRemaining > 0 && c.readErr == io.EOF {
c.readErr = errUnexpectedEOF c.readErr = errUnexpectedEOF
} }
@ -1041,7 +1077,7 @@ func (c *Conn) SetReadDeadline(t time.Time) error {
return c.conn.SetReadDeadline(t) return c.conn.SetReadDeadline(t)
} }
// SetReadLimit sets the maximum size for a message read from the peer. If a // SetReadLimit sets the maximum size in bytes for a message read from the peer. If a
// message exceeds the limit, the connection sends a close message to the peer // message exceeds the limit, the connection sends a close message to the peer
// and returns ErrReadLimit to the application. // and returns ErrReadLimit to the application.
func (c *Conn) SetReadLimit(limit int64) { func (c *Conn) SetReadLimit(limit int64) {

View File

@ -151,6 +151,53 @@
// checking. The application is responsible for checking the Origin header // checking. The application is responsible for checking the Origin header
// before calling the Upgrade function. // before calling the Upgrade function.
// //
// Buffers
//
// Connections buffer network input and output to reduce the number
// of system calls when reading or writing messages.
//
// Write buffers are also used for constructing WebSocket frames. See RFC 6455,
// Section 5 for a discussion of message framing. A WebSocket frame header is
// written to the network each time a write buffer is flushed to the network.
// Decreasing the size of the write buffer can increase the amount of framing
// overhead on the connection.
//
// The buffer sizes in bytes are specified by the ReadBufferSize and
// WriteBufferSize fields in the Dialer and Upgrader. The Dialer uses a default
// size of 4096 when a buffer size field is set to zero. The Upgrader reuses
// buffers created by the HTTP server when a buffer size field is set to zero.
// The HTTP server buffers have a size of 4096 at the time of this writing.
//
// The buffer sizes do not limit the size of a message that can be read or
// written by a connection.
//
// Buffers are held for the lifetime of the connection by default. If the
// Dialer or Upgrader WriteBufferPool field is set, then a connection holds the
// write buffer only when writing a message.
//
// Applications should tune the buffer sizes to balance memory use and
// performance. Increasing the buffer size uses more memory, but can reduce the
// number of system calls to read or write the network. In the case of writing,
// increasing the buffer size can reduce the number of frame headers written to
// the network.
//
// Some guidelines for setting buffer parameters are:
//
// Limit the buffer sizes to the maximum expected message size. Buffers larger
// than the largest message do not provide any benefit.
//
// Depending on the distribution of message sizes, setting the buffer size to
// to a value less than the maximum expected message size can greatly reduce
// memory use with a small impact on performance. Here's an example: If 99% of
// the messages are smaller than 256 bytes and the maximum message size is 512
// bytes, then a buffer size of 256 bytes will result in 1.01 more system calls
// than a buffer size of 512 bytes. The memory savings is 50%.
//
// A write buffer pool is useful when the application has a modest number
// writes over a large number of connections. when buffers are pooled, a larger
// buffer size has a reduced impact on total memory use and has the benefit of
// reducing system calls and frame overhead.
//
// Compression EXPERIMENTAL // Compression EXPERIMENTAL
// //
// Per message compression extensions (RFC 7692) are experimentally supported // Per message compression extensions (RFC 7692) are experimentally supported

3
vendor/github.com/gorilla/websocket/go.mod generated vendored Normal file
View File

@ -0,0 +1,3 @@
module github.com/gorilla/websocket
go 1.12

2
vendor/github.com/gorilla/websocket/go.sum generated vendored Normal file
View File

@ -0,0 +1,2 @@
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=

42
vendor/github.com/gorilla/websocket/join.go generated vendored Normal file
View File

@ -0,0 +1,42 @@
// Copyright 2019 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.
package websocket
import (
"io"
"strings"
)
// JoinMessages concatenates received messages to create a single io.Reader.
// The string term is appended to each message. The returned reader does not
// support concurrent calls to the Read method.
func JoinMessages(c *Conn, term string) io.Reader {
return &joinReader{c: c, term: term}
}
type joinReader struct {
c *Conn
term string
r io.Reader
}
func (r *joinReader) Read(p []byte) (int, error) {
if r.r == nil {
var err error
_, r.r, err = r.c.NextReader()
if err != nil {
return 0, err
}
if r.term != "" {
r.r = io.MultiReader(r.r, strings.NewReader(r.term))
}
}
n, err := r.r.Read(p)
if err == io.EOF {
err = nil
r.r = nil
}
return n, err
}

View File

@ -22,18 +22,18 @@ func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) {
func init() { func init() {
proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) { proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) {
return &httpProxyDialer{proxyURL: proxyURL, fowardDial: forwardDialer.Dial}, nil return &httpProxyDialer{proxyURL: proxyURL, forwardDial: forwardDialer.Dial}, nil
}) })
} }
type httpProxyDialer struct { type httpProxyDialer struct {
proxyURL *url.URL proxyURL *url.URL
fowardDial func(network, addr string) (net.Conn, error) forwardDial func(network, addr string) (net.Conn, error)
} }
func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) { func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) {
hostPort, _ := hostPortNoPort(hpd.proxyURL) hostPort, _ := hostPortNoPort(hpd.proxyURL)
conn, err := hpd.fowardDial(network, hostPort) conn, err := hpd.forwardDial(network, hostPort)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -27,7 +27,7 @@ type Upgrader struct {
// HandshakeTimeout specifies the duration for the handshake to complete. // HandshakeTimeout specifies the duration for the handshake to complete.
HandshakeTimeout time.Duration HandshakeTimeout time.Duration
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer // ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer
// size is zero, then buffers allocated by the HTTP server are used. The // size is zero, then buffers allocated by the HTTP server are used. The
// I/O buffer sizes do not limit the size of the messages that can be sent // I/O buffer sizes do not limit the size of the messages that can be sent
// or received. // or received.
@ -153,7 +153,7 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
challengeKey := r.Header.Get("Sec-Websocket-Key") challengeKey := r.Header.Get("Sec-Websocket-Key")
if challengeKey == "" { if challengeKey == "" {
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-WebSocket-Key' header is missing or blank") return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header is missing or blank")
} }
subprotocol := u.selectSubprotocol(r, responseHeader) subprotocol := u.selectSubprotocol(r, responseHeader)

View File

@ -31,68 +31,113 @@ func generateChallengeKey() (string, error) {
return base64.StdEncoding.EncodeToString(p), nil return base64.StdEncoding.EncodeToString(p), nil
} }
// Octet types from RFC 2616. // Token octets per RFC 2616.
var octetTypes [256]byte var isTokenOctet = [256]bool{
'!': true,
const ( '#': true,
isTokenOctet = 1 << iota '$': true,
isSpaceOctet '%': true,
) '&': true,
'\'': true,
func init() { '*': true,
// From RFC 2616 '+': true,
// '-': true,
// OCTET = <any 8-bit sequence of data> '.': true,
// CHAR = <any US-ASCII character (octets 0 - 127)> '0': true,
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)> '1': true,
// CR = <US-ASCII CR, carriage return (13)> '2': true,
// LF = <US-ASCII LF, linefeed (10)> '3': true,
// SP = <US-ASCII SP, space (32)> '4': true,
// HT = <US-ASCII HT, horizontal-tab (9)> '5': true,
// <"> = <US-ASCII double-quote mark (34)> '6': true,
// CRLF = CR LF '7': true,
// LWS = [CRLF] 1*( SP | HT ) '8': true,
// TEXT = <any OCTET except CTLs, but including LWS> '9': true,
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> 'A': true,
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT 'B': true,
// token = 1*<any CHAR except CTLs or separators> 'C': true,
// qdtext = <any TEXT except <">> 'D': true,
'E': true,
for c := 0; c < 256; c++ { 'F': true,
var t byte 'G': true,
isCtl := c <= 31 || c == 127 'H': true,
isChar := 0 <= c && c <= 127 'I': true,
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0 'J': true,
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 { 'K': true,
t |= isSpaceOctet 'L': true,
} 'M': true,
if isChar && !isCtl && !isSeparator { 'N': true,
t |= isTokenOctet 'O': true,
} 'P': true,
octetTypes[c] = t 'Q': true,
} 'R': true,
'S': true,
'T': true,
'U': true,
'W': true,
'V': true,
'X': true,
'Y': true,
'Z': true,
'^': true,
'_': true,
'`': true,
'a': true,
'b': true,
'c': true,
'd': true,
'e': true,
'f': true,
'g': true,
'h': true,
'i': true,
'j': true,
'k': true,
'l': true,
'm': true,
'n': true,
'o': true,
'p': true,
'q': true,
'r': true,
's': true,
't': true,
'u': true,
'v': true,
'w': true,
'x': true,
'y': true,
'z': true,
'|': true,
'~': true,
} }
// skipSpace returns a slice of the string s with all leading RFC 2616 linear
// whitespace removed.
func skipSpace(s string) (rest string) { func skipSpace(s string) (rest string) {
i := 0 i := 0
for ; i < len(s); i++ { for ; i < len(s); i++ {
if octetTypes[s[i]]&isSpaceOctet == 0 { if b := s[i]; b != ' ' && b != '\t' {
break break
} }
} }
return s[i:] return s[i:]
} }
// nextToken returns the leading RFC 2616 token of s and the string following
// the token.
func nextToken(s string) (token, rest string) { func nextToken(s string) (token, rest string) {
i := 0 i := 0
for ; i < len(s); i++ { for ; i < len(s); i++ {
if octetTypes[s[i]]&isTokenOctet == 0 { if !isTokenOctet[s[i]] {
break break
} }
} }
return s[:i], s[i:] return s[:i], s[i:]
} }
// nextTokenOrQuoted returns the leading token or quoted string per RFC 2616
// and the string following the token or quoted string.
func nextTokenOrQuoted(s string) (value string, rest string) { func nextTokenOrQuoted(s string) (value string, rest string) {
if !strings.HasPrefix(s, "\"") { if !strings.HasPrefix(s, "\"") {
return nextToken(s) return nextToken(s)
@ -128,7 +173,8 @@ func nextTokenOrQuoted(s string) (value string, rest string) {
return "", "" return "", ""
} }
// equalASCIIFold returns true if s is equal to t with ASCII case folding. // equalASCIIFold returns true if s is equal to t with ASCII case folding as
// defined in RFC 4790.
func equalASCIIFold(s, t string) bool { func equalASCIIFold(s, t string) bool {
for s != "" && t != "" { for s != "" && t != "" {
sr, size := utf8.DecodeRuneInString(s) sr, size := utf8.DecodeRuneInString(s)

View File

@ -1,3 +1,20 @@
### v0.6.0 - August 31, 2019
full differences can be viewed using `git log --oneline --decorate --color v0.5.0..v0.6.0`
thanks to everyone who has contributed since January!
#### Breaking Changes:
- Info struct has had fields removed related to deprecated functionality by slack.
- minor adjustments to some structs.
- some internal default values have changed, usually to be more inline with slack defaults or to correct inability to set a particular value. (Message Parse for example.)
##### Highlights:
- new slacktest package easy mocking for slack client. use, enjoy, please submit PRs for improvements and default behaviours! shamelessly taken from the [slack-test repo](https://github.com/lusis/slack-test) thank you lusis for letting us use it and bring it into the slack repo.
- blocks, blocks, blocks.
- RTM ManagedConnection has undergone a significant cleanup.
in particular handles backoffs gracefully, removed many deadlocks,
and Disconnect is now much more responsive.
### v0.5.0 - January 20, 2019 ### v0.5.0 - January 20, 2019
full differences can be viewed using `git log --oneline --decorate --color v0.4.0..v0.5.0` 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. - Breaking changes: various old struct fields have been removed or updated to match slack's api.

View File

@ -30,12 +30,22 @@ type Blocks struct {
// BlockAction is the action callback sent when a block is interacted with // BlockAction is the action callback sent when a block is interacted with
type BlockAction struct { type BlockAction struct {
ActionID string `json:"action_id"` ActionID string `json:"action_id"`
BlockID string `json:"block_id"` BlockID string `json:"block_id"`
Text TextBlockObject `json:"text"` Type actionType `json:"type"`
Value string `json:"value"` Text TextBlockObject `json:"text"`
Type actionType `json:"type"` Value string `json:"value"`
ActionTs string `json:"action_ts"` ActionTs string `json:"action_ts"`
SelectedOption OptionBlockObject `json:"selected_option"`
SelectedUser string `json:"selected_user"`
SelectedChannel string `json:"selected_channel"`
SelectedConversation string `json:"selected_conversation"`
SelectedDate string `json:"selected_date"`
InitialOption OptionBlockObject `json:"initial_option"`
InitialUser string `json:"initial_user"`
InitialChannel string `json:"initial_channel"`
InitialConversation string `json:"initial_conversation"`
InitialDate string `json:"initial_date"`
} }
// actionType returns the type of the action // actionType returns the type of the action

View File

@ -124,7 +124,7 @@ func (b *BlockElements) UnmarshalJSON(data []byte) error {
blockElement = &OverflowBlockElement{} blockElement = &OverflowBlockElement{}
case "datepicker": case "datepicker":
blockElement = &DatePickerBlockElement{} blockElement = &DatePickerBlockElement{}
case "static_select": case "static_select", "external_select", "users_select", "conversations_select", "channels_select":
blockElement = &SelectBlockElement{} blockElement = &SelectBlockElement{}
default: default:
return errors.New("unsupported block element type") return errors.New("unsupported block element type")

View File

@ -139,13 +139,17 @@ func NewButtonBlockElement(actionID, value string, text *TextBlockObject) *Butto
// //
// More Information: https://api.slack.com/reference/messaging/block-elements#select // More Information: https://api.slack.com/reference/messaging/block-elements#select
type SelectBlockElement struct { type SelectBlockElement struct {
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Placeholder *TextBlockObject `json:"placeholder,omitempty"` Placeholder *TextBlockObject `json:"placeholder,omitempty"`
ActionID string `json:"action_id,omitempty"` ActionID string `json:"action_id,omitempty"`
Options []*OptionBlockObject `json:"options,omitempty"` Options []*OptionBlockObject `json:"options,omitempty"`
OptionGroups []*OptionGroupBlockObject `json:"option_groups,omitempty"` OptionGroups []*OptionGroupBlockObject `json:"option_groups,omitempty"`
InitialOption *OptionBlockObject `json:"initial_option,omitempty"` InitialOption *OptionBlockObject `json:"initial_option,omitempty"`
Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` InitialUser string `json:"initial_user,omitempty"`
InitialConversation string `json:"initial_conversation,omitempty"`
InitialChannel string `json:"initial_channel,omitempty"`
MinQueryLength int `json:"min_query_length,omitempty"`
Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
} }
// ElementType returns the type of the Element // ElementType returns the type of the Element

View File

@ -178,6 +178,7 @@ func NewConfirmationBlockObject(title, text, confirm, deny *TextBlockObject) *Co
type OptionBlockObject struct { type OptionBlockObject struct {
Text *TextBlockObject `json:"text"` Text *TextBlockObject `json:"text"`
Value string `json:"value"` Value string `json:"value"`
URL string `json:"url"`
} }
// NewOptionBlockObject returns an instance of a new Option Block Element // NewOptionBlockObject returns an instance of a new Option Block Element
@ -197,7 +198,7 @@ func (s OptionBlockObject) validateType() MessageObjectType {
// //
// More Information: https://api.slack.com/reference/messaging/composition-objects#option-group // More Information: https://api.slack.com/reference/messaging/composition-objects#option-group
type OptionGroupBlockObject struct { type OptionGroupBlockObject struct {
Label *TextBlockObject `json:"label"` Label *TextBlockObject `json:"label,omitempty"`
Options []*OptionBlockObject `json:"options"` Options []*OptionBlockObject `json:"options"`
} }

View File

@ -16,12 +16,27 @@ func (s SectionBlock) BlockType() MessageBlockType {
return s.Type return s.Type
} }
// SectionBlockOption allows configuration of options for a new section block
type SectionBlockOption func(*SectionBlock)
func SectionBlockOptionBlockID(blockID string) SectionBlockOption {
return func(block *SectionBlock) {
block.BlockID = blockID
}
}
// NewSectionBlock returns a new instance of a section block to be rendered // NewSectionBlock returns a new instance of a section block to be rendered
func NewSectionBlock(textObj *TextBlockObject, fields []*TextBlockObject, accessory *Accessory) *SectionBlock { func NewSectionBlock(textObj *TextBlockObject, fields []*TextBlockObject, accessory *Accessory, options ...SectionBlockOption) *SectionBlock {
return &SectionBlock{ block := SectionBlock{
Type: MBTSection, Type: MBTSection,
Text: textObj, Text: textObj,
Fields: fields, Fields: fields,
Accessory: accessory, Accessory: accessory,
} }
for _, option := range options {
option(&block)
}
return &block
} }

View File

@ -41,7 +41,10 @@ func (api *Client) GetBotInfo(bot string) (*Bot, error) {
func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, error) { func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, error) {
values := url.Values{ values := url.Values{
"token": {api.token}, "token": {api.token},
"bot": {bot}, }
if bot != "" {
values.Add("bot", bot)
} }
response, err := api.botRequest(ctx, "bots.info", values) response, err := api.botRequest(ctx, "bots.info", values)

View File

@ -32,11 +32,7 @@ func (api *Client) channelRequest(ctx context.Context, path string, values url.V
return nil, err return nil, err
} }
if err := response.Err(); err != nil { return response, response.Err()
return nil, err
}
return response, nil
} }
type channelsConfig struct { type channelsConfig struct {
@ -284,6 +280,7 @@ func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool,
"token": {api.token}, "token": {api.token},
}, },
} }
if excludeArchived { if excludeArchived {
options = append(options, GetChannelsOptionExcludeArchived()) options = append(options, GetChannelsOptionExcludeArchived())
} }

View File

@ -3,6 +3,7 @@ package slack
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"net/http"
"net/url" "net/url"
"github.com/nlopes/slack/slackutilsx" "github.com/nlopes/slack/slackutilsx"
@ -25,7 +26,7 @@ const (
type chatResponseFull struct { type chatResponseFull struct {
Channel string `json:"channel"` Channel string `json:"channel"`
Timestamp string `json:"ts"` //Regualr message timestamp Timestamp string `json:"ts"` //Regular message timestamp
MessageTimeStamp string `json:"message_ts"` //Ephemeral message timestamp MessageTimeStamp string `json:"message_ts"` //Ephemeral message timestamp
Text string `json:"text"` Text string `json:"text"`
SlackResponse SlackResponse
@ -156,17 +157,18 @@ func (api *Client) SendMessage(channel string, options ...MsgOption) (string, st
} }
// SendMessageContext more flexible method for configuring messages with a custom context. // SendMessageContext more flexible method for configuring messages with a custom context.
func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (channel string, timestamp string, text string, err error) { func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (_channel string, _timestamp string, _text string, err error) {
var ( var (
config sendConfig req *http.Request
parser func(*chatResponseFull) responseParser
response chatResponseFull response chatResponseFull
) )
if config, err = applyMsgOptions(api.token, channelID, api.endpoint, options...); err != nil { if req, parser, err = buildSender(api.endpoint, options...).BuildRequest(api.token, channelID); err != nil {
return "", "", "", err return "", "", "", err
} }
if err = postForm(ctx, api.httpclient, config.endpoint, config.values, &response, api); err != nil { if err = doPost(ctx, api.httpclient, req, parser(&response), api); err != nil {
return "", "", "", err return "", "", "", err
} }
@ -200,6 +202,13 @@ func applyMsgOptions(token, channel, apiurl string, options ...MsgOption) (sendC
return config, nil return config, nil
} }
func buildSender(apiurl string, options ...MsgOption) sendConfig {
return sendConfig{
apiurl: apiurl,
options: options,
}
}
type sendMode string type sendMode string
const ( const (
@ -207,16 +216,70 @@ const (
chatPostMessage sendMode = "chat.postMessage" chatPostMessage sendMode = "chat.postMessage"
chatDelete sendMode = "chat.delete" chatDelete sendMode = "chat.delete"
chatPostEphemeral sendMode = "chat.postEphemeral" chatPostEphemeral sendMode = "chat.postEphemeral"
chatResponse sendMode = "chat.responseURL"
chatMeMessage sendMode = "chat.meMessage" chatMeMessage sendMode = "chat.meMessage"
chatUnfurl sendMode = "chat.unfurl" chatUnfurl sendMode = "chat.unfurl"
) )
type sendConfig struct { type sendConfig struct {
apiurl string apiurl string
options []MsgOption
mode sendMode
endpoint string
values url.Values
attachments []Attachment
responseType string
}
func (t sendConfig) BuildRequest(token, channelID string) (req *http.Request, _ func(*chatResponseFull) responseParser, err error) {
if t, err = applyMsgOptions(token, channelID, t.apiurl, t.options...); err != nil {
return nil, nil, err
}
switch t.mode {
case chatResponse:
return responseURLSender{
endpoint: t.endpoint,
values: t.values,
attachments: t.attachments,
responseType: t.responseType,
}.BuildRequest()
default:
return formSender{endpoint: t.endpoint, values: t.values}.BuildRequest()
}
}
type formSender struct {
endpoint string endpoint string
values url.Values values url.Values
} }
func (t formSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) {
req, err := formReq(t.endpoint, t.values)
return req, func(resp *chatResponseFull) responseParser {
return newJSONParser(resp)
}, err
}
type responseURLSender struct {
endpoint string
values url.Values
attachments []Attachment
responseType string
}
func (t responseURLSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) {
req, err := jsonReq(t.endpoint, Msg{
Text: t.values.Get("text"),
Timestamp: t.values.Get("ts"),
Attachments: t.attachments,
ResponseType: t.responseType,
})
return req, func(resp *chatResponseFull) responseParser {
return newContentTypeParser(resp)
}, err
}
// MsgOption option provided when sending a message. // MsgOption option provided when sending a message.
type MsgOption func(*sendConfig) error type MsgOption func(*sendConfig) error
@ -279,6 +342,17 @@ func MsgOptionUnfurl(timestamp string, unfurls map[string]Attachment) MsgOption
} }
} }
// MsgOptionResponseURL supplies a url to use as the endpoint.
func MsgOptionResponseURL(url string, rt string) MsgOption {
return func(config *sendConfig) error {
config.mode = chatResponse
config.endpoint = url
config.responseType = rt
config.values.Del("ts")
return nil
}
}
// MsgOptionAsUser whether or not to send the message as the user. // MsgOptionAsUser whether or not to send the message as the user.
func MsgOptionAsUser(b bool) MsgOption { func MsgOptionAsUser(b bool) MsgOption {
return func(config *sendConfig) error { return func(config *sendConfig) error {
@ -324,10 +398,17 @@ func MsgOptionAttachments(attachments ...Attachment) MsgOption {
return nil return nil
} }
attachments, err := json.Marshal(attachments) config.attachments = attachments
// FIXME: We are setting the attachments on the message twice: above for
// the json version, and below for the html version. The marshalled bytes
// we put into config.values below don't work directly in the Msg version.
attachmentBytes, err := json.Marshal(attachments)
if err == nil { if err == nil {
config.values.Set("attachments", string(attachments)) config.values.Set("attachments", string(attachmentBytes))
} }
return err return err
} }
} }

View File

@ -99,6 +99,7 @@ func (api *Client) GetUsersInConversationContext(ctx context.Context, params *Ge
ResponseMetaData responseMetaData `json:"response_metadata"` ResponseMetaData responseMetaData `json:"response_metadata"`
SlackResponse SlackResponse
}{} }{}
err := api.postMethod(ctx, "conversations.members", values, &response) err := api.postMethod(ctx, "conversations.members", values, &response)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
@ -160,6 +161,7 @@ func (api *Client) ArchiveConversationContext(ctx context.Context, channelID str
"token": {api.token}, "token": {api.token},
"channel": {channelID}, "channel": {channelID},
} }
response := SlackResponse{} response := SlackResponse{}
err := api.postMethod(ctx, "conversations.archive", values, &response) err := api.postMethod(ctx, "conversations.archive", values, &response)
if err != nil { if err != nil {
@ -229,6 +231,7 @@ func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelI
SlackResponse SlackResponse
Channel *Channel `json:"channel"` Channel *Channel `json:"channel"`
}{} }{}
err := api.postMethod(ctx, "conversations.setPurpose", values, &response) err := api.postMethod(ctx, "conversations.setPurpose", values, &response)
if err != nil { if err != nil {
return nil, err return nil, err
@ -253,6 +256,7 @@ func (api *Client) RenameConversationContext(ctx context.Context, channelID, cha
SlackResponse SlackResponse
Channel *Channel `json:"channel"` Channel *Channel `json:"channel"`
}{} }{}
err := api.postMethod(ctx, "conversations.rename", values, &response) err := api.postMethod(ctx, "conversations.rename", values, &response)
if err != nil { if err != nil {
return nil, err return nil, err
@ -277,6 +281,7 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel
SlackResponse SlackResponse
Channel *Channel `json:"channel"` Channel *Channel `json:"channel"`
}{} }{}
err := api.postMethod(ctx, "conversations.invite", values, &response) err := api.postMethod(ctx, "conversations.invite", values, &response)
if err != nil { if err != nil {
return nil, err return nil, err
@ -297,6 +302,7 @@ func (api *Client) KickUserFromConversationContext(ctx context.Context, channelI
"channel": {channelID}, "channel": {channelID},
"user": {user}, "user": {user},
} }
response := SlackResponse{} response := SlackResponse{}
err := api.postMethod(ctx, "conversations.kick", values, &response) err := api.postMethod(ctx, "conversations.kick", values, &response)
if err != nil { if err != nil {
@ -479,6 +485,7 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve
ResponseMetaData responseMetaData `json:"response_metadata"` ResponseMetaData responseMetaData `json:"response_metadata"`
SlackResponse SlackResponse
}{} }{}
err = api.postMethod(ctx, "conversations.list", values, &response) err = api.postMethod(ctx, "conversations.list", values, &response)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
@ -516,6 +523,7 @@ func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConv
AlreadyOpen bool `json:"already_open"` AlreadyOpen bool `json:"already_open"`
SlackResponse SlackResponse
}{} }{}
err := api.postMethod(ctx, "conversations.open", values, &response) err := api.postMethod(ctx, "conversations.open", values, &response)
if err != nil { if err != nil {
return nil, false, false, err return nil, false, false, err
@ -540,6 +548,7 @@ func (api *Client) JoinConversationContext(ctx context.Context, channelID string
} `json:"response_metadata"` } `json:"response_metadata"`
SlackResponse SlackResponse
}{} }{}
err := api.postMethod(ctx, "conversations.join", values, &response) err := api.postMethod(ctx, "conversations.join", values, &response)
if err != nil { if err != nil {
return nil, "", nil, err return nil, "", nil, err

View File

@ -21,7 +21,7 @@ type DialogInputSelect struct {
DialogInput DialogInput
Value string `json:"value,omitempty"` //Optional. Value string `json:"value,omitempty"` //Optional.
DataSource SelectDataSource `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external". 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 SelectedOptions []DialogSelectOption `json:"selected_options,omitempty"` //Optional. May hold at most one element, for use with "external" only.
Options []DialogSelectOption `json:"options,omitempty"` //One of options or option_groups is required. Options []DialogSelectOption `json:"options,omitempty"` //One of options or option_groups is required.
OptionGroups []DialogOptionGroup `json:"option_groups,omitempty"` //Provide up to 100 options. 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. MinQueryLength int `json:"min_query_length,omitempty"` //Optional. minimum characters before query is sent.

View File

@ -3,6 +3,9 @@ package slack
// TextInputSubtype Accepts email, number, tel, or url. In some form factors, optimized input is provided for this subtype. // TextInputSubtype Accepts email, number, tel, or url. In some form factors, optimized input is provided for this subtype.
type TextInputSubtype string type TextInputSubtype string
// TextInputOption handle to extra inputs options.
type TextInputOption func(*TextInputElement)
const ( const (
// InputSubtypeEmail email keyboard // InputSubtypeEmail email keyboard
InputSubtypeEmail TextInputSubtype = "email" InputSubtypeEmail TextInputSubtype = "email"
@ -26,8 +29,8 @@ type TextInputElement struct {
} }
// NewTextInput constructor for a `text` input // NewTextInput constructor for a `text` input
func NewTextInput(name, label, text string) *TextInputElement { func NewTextInput(name, label, text string, options ...TextInputOption) *TextInputElement {
return &TextInputElement{ t := &TextInputElement{
DialogInput: DialogInput{ DialogInput: DialogInput{
Type: InputTypeText, Type: InputTypeText,
Name: name, Name: name,
@ -35,6 +38,12 @@ func NewTextInput(name, label, text string) *TextInputElement {
}, },
Value: text, Value: text,
} }
for _, opt := range options {
opt(t)
}
return t
} }
// NewTextAreaInput constructor for a `textarea` input // NewTextAreaInput constructor for a `textarea` input

View File

@ -90,7 +90,8 @@ type File struct {
} }
type Share struct { type Share struct {
Public map[string][]ShareFileInfo `json:"public"` Public map[string][]ShareFileInfo `json:"public"`
Private map[string][]ShareFileInfo `json:"private"`
} }
type ShareFileInfo struct { type ShareFileInfo struct {
@ -133,11 +134,21 @@ type GetFilesParameters struct {
Page int Page int
} }
// ListFilesParameters contains all the parameters necessary (including the optional ones) for a ListFiles() request
type ListFilesParameters struct {
Limit int
User string
Channel string
Types string
Cursor string
}
type fileResponseFull struct { type fileResponseFull struct {
File `json:"file"` File `json:"file"`
Paging `json:"paging"` Paging `json:"paging"`
Comments []Comment `json:"comments"` Comments []Comment `json:"comments"`
Files []File `json:"files"` Files []File `json:"files"`
Metadata ResponseMetadata `json:"response_metadata"`
SlackResponse SlackResponse
} }
@ -196,6 +207,40 @@ func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error)
return api.GetFilesContext(context.Background(), params) return api.GetFilesContext(context.Background(), params)
} }
// ListFiles retrieves all files according to the parameters given. Uses cursor based pagination.
func (api *Client) ListFiles(params ListFilesParameters) ([]File, *ListFilesParameters, error) {
return api.ListFilesContext(context.Background(), params)
}
// ListFilesContext retrieves all files according to the parameters given with a custom context. Uses cursor based pagination.
func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParameters) ([]File, *ListFilesParameters, error) {
values := url.Values{
"token": {api.token},
}
if params.User != DEFAULT_FILES_USER {
values.Add("user", params.User)
}
if params.Channel != DEFAULT_FILES_CHANNEL {
values.Add("channel", params.Channel)
}
if params.Limit != DEFAULT_FILES_COUNT {
values.Add("limit", strconv.Itoa(params.Limit))
}
if params.Cursor != "" {
values.Add("cursor", params.Cursor)
}
response, err := api.fileRequest(ctx, "files.list", values)
if err != nil {
return nil, nil, err
}
params.Cursor = response.Metadata.Cursor
return response.Files, &params, nil
}
// GetFilesContext retrieves all files according to the parameters given with a custom context // GetFilesContext retrieves all files according to the parameters given with a custom context
func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameters) ([]File, *Paging, error) { func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameters) ([]File, *Paging, error) {
values := url.Values{ values := url.Values{
@ -243,9 +288,6 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam
if err != nil { if err != nil {
return nil, err return nil, err
} }
if params.Filename == "" {
return nil, fmt.Errorf("files.upload: FileUploadParameters.Filename is mandatory")
}
response := &fileResponseFull{} response := &fileResponseFull{}
values := url.Values{ values := url.Values{
"token": {api.token}, "token": {api.token},
@ -274,8 +316,12 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam
} else if params.File != "" { } else if params.File != "" {
err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", values, response, api) err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", values, response, api)
} else if params.Reader != nil { } else if params.Reader != nil {
if params.Filename == "" {
return nil, fmt.Errorf("files.upload: FileUploadParameters.Filename is mandatory when using FileUploadParameters.Reader")
}
err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", values, params.Reader, response, api) err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", values, params.Reader, response, api)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -156,17 +156,12 @@ type Icons struct {
Image72 string `json:"image_72,omitempty"` Image72 string `json:"image_72,omitempty"`
} }
// Info contains various details about Users, Channels, Bots and the authenticated user. // Info contains various details about the authenticated user and team.
// It is returned by StartRTM or included in the "ConnectedEvent" RTM event. // It is returned by StartRTM or included in the "ConnectedEvent" RTM event.
type Info struct { type Info struct {
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
User *UserDetails `json:"self,omitempty"` User *UserDetails `json:"self,omitempty"`
Team *Team `json:"team,omitempty"` Team *Team `json:"team,omitempty"`
Users []User `json:"users,omitempty"`
Channels []Channel `json:"channels,omitempty"`
Groups []Group `json:"groups,omitempty"`
Bots []Bot `json:"bots,omitempty"`
IMs []IM `json:"ims,omitempty"`
} }
type infoResponseFull struct { type infoResponseFull struct {
@ -174,52 +169,27 @@ type infoResponseFull struct {
SlackResponse SlackResponse
} }
// GetBotByID returns a bot given a bot id // GetBotByID is deprecated and returns nil
func (info Info) GetBotByID(botID string) *Bot { func (info Info) GetBotByID(botID string) *Bot {
for _, bot := range info.Bots {
if bot.ID == botID {
return &bot
}
}
return nil return nil
} }
// GetUserByID returns a user given a user id // GetUserByID is deprecated and returns nil
func (info Info) GetUserByID(userID string) *User { func (info Info) GetUserByID(userID string) *User {
for _, user := range info.Users {
if user.ID == userID {
return &user
}
}
return nil return nil
} }
// GetChannelByID returns a channel given a channel id // GetChannelByID is deprecated and returns nil
func (info Info) GetChannelByID(channelID string) *Channel { func (info Info) GetChannelByID(channelID string) *Channel {
for _, channel := range info.Channels {
if channel.ID == channelID {
return &channel
}
}
return nil return nil
} }
// GetGroupByID returns a group given a group id // GetGroupByID is deprecated and returns nil
func (info Info) GetGroupByID(groupID string) *Group { func (info Info) GetGroupByID(groupID string) *Group {
for _, group := range info.Groups {
if group.ID == groupID {
return &group
}
}
return nil return nil
} }
// GetIMByID returns an IM given an IM id // GetIMByID is deprecated and returns nil
func (info Info) GetIMByID(imID string) *IM { func (info Info) GetIMByID(imID string) *IM {
for _, im := range info.IMs {
if im.ID == imID {
return &im
}
}
return nil return nil
} }

View File

@ -22,6 +22,7 @@ const (
InteractionTypeDialogSuggestion = InteractionType("dialog_suggestion") InteractionTypeDialogSuggestion = InteractionType("dialog_suggestion")
InteractionTypeInteractionMessage = InteractionType("interactive_message") InteractionTypeInteractionMessage = InteractionType("interactive_message")
InteractionTypeMessageAction = InteractionType("message_action") InteractionTypeMessageAction = InteractionType("message_action")
InteractionTypeBlockActions = InteractionType("block_actions")
) )
// InteractionCallback is sent from slack when a user interactions with a button or dialog. // InteractionCallback is sent from slack when a user interactions with a button or dialog.

View File

@ -98,6 +98,13 @@ type Msg struct {
Blocks Blocks `json:"blocks,omitempty"` Blocks Blocks `json:"blocks,omitempty"`
} }
const (
// ResponseTypeInChannel in channel response for slash commands.
ResponseTypeInChannel = "in_channel"
// ResponseTypeEphemeral ephemeral respone for slash commands.
ResponseTypeEphemeral = "ephemeral"
)
// Icon is used for bot messages // Icon is used for bot messages
type Icon struct { type Icon struct {
IconURL string `json:"icon_url,omitempty"` IconURL string `json:"icon_url,omitempty"`

View File

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"mime"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@ -80,8 +81,8 @@ func fileUploadReq(ctx context.Context, path string, values url.Values, r io.Rea
if err != nil { if err != nil {
return nil, err return nil, err
} }
req = req.WithContext(ctx)
req = req.WithContext(ctx)
req.URL.RawQuery = (values).Encode() req.URL.RawQuery = (values).Encode()
return req, nil return req, nil
} }
@ -117,6 +118,29 @@ func downloadFile(client httpClient, token string, downloadURL string, writer io
return err return err
} }
func formReq(endpoint string, values url.Values) (req *http.Request, err error) {
if req, err = http.NewRequest("POST", endpoint, strings.NewReader(values.Encode())); err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return req, nil
}
func jsonReq(endpoint string, body interface{}) (req *http.Request, err error) {
buffer := bytes.NewBuffer([]byte{})
if err = json.NewEncoder(buffer).Encode(body); err != nil {
return nil, err
}
if req, err = http.NewRequest("POST", endpoint, buffer); err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
return req, nil
}
func parseResponseBody(body io.ReadCloser, intf interface{}, d debug) error { func parseResponseBody(body io.ReadCloser, intf interface{}, d debug) error {
response, err := ioutil.ReadAll(body) response, err := ioutil.ReadAll(body)
if err != nil { if err != nil {
@ -130,7 +154,7 @@ func parseResponseBody(body io.ReadCloser, intf interface{}, d debug) error {
return json.Unmarshal(response, intf) return json.Unmarshal(response, intf)
} }
func postLocalWithMultipartResponse(ctx context.Context, client httpClient, path, fpath, fieldname string, values url.Values, intf interface{}, d debug) error { func postLocalWithMultipartResponse(ctx context.Context, client httpClient, method, fpath, fieldname string, values url.Values, intf interface{}, d debug) error {
fullpath, err := filepath.Abs(fpath) fullpath, err := filepath.Abs(fpath)
if err != nil { if err != nil {
return err return err
@ -140,7 +164,8 @@ func postLocalWithMultipartResponse(ctx context.Context, client httpClient, path
return err return err
} }
defer file.Close() defer file.Close()
return postWithMultipartResponse(ctx, client, path, filepath.Base(fpath), fieldname, values, file, intf, d)
return postWithMultipartResponse(ctx, client, method, filepath.Base(fpath), fieldname, values, file, intf, d)
} }
func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, d debug) error { func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, d debug) error {
@ -186,11 +211,11 @@ func postWithMultipartResponse(ctx context.Context, client httpClient, path, nam
case err = <-errc: case err = <-errc:
return err return err
default: default:
return parseResponseBody(resp.Body, intf, d) return newJSONParser(intf)(resp)
} }
} }
func doPost(ctx context.Context, client httpClient, req *http.Request, intf interface{}, d debug) error { func doPost(ctx context.Context, client httpClient, req *http.Request, parser responseParser, d debug) error {
req = req.WithContext(ctx) req = req.WithContext(ctx)
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
@ -203,7 +228,7 @@ func doPost(ctx context.Context, client httpClient, req *http.Request, intf inte
return err return err
} }
return parseResponseBody(resp.Body, intf, d) return parser(resp)
} }
// post JSON. // post JSON.
@ -215,7 +240,8 @@ func postJSON(ctx context.Context, client httpClient, endpoint, token string, js
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return doPost(ctx, client, req, intf, d)
return doPost(ctx, client, req, newJSONParser(intf), d)
} }
// post a url encoded form. // post a url encoded form.
@ -226,7 +252,7 @@ func postForm(ctx context.Context, client httpClient, endpoint string, values ur
return err return err
} }
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return doPost(ctx, client, req, intf, d) return doPost(ctx, client, req, newJSONParser(intf), d)
} }
func getResource(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d debug) error { func getResource(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d debug) error {
@ -237,7 +263,7 @@ func getResource(ctx context.Context, client httpClient, endpoint string, values
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.URL.RawQuery = values.Encode() req.URL.RawQuery = values.Encode()
return doPost(ctx, client, req, intf, d) return doPost(ctx, client, req, newJSONParser(intf), d)
} }
func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf interface{}, d debug) error { func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf interface{}, d debug) error {
@ -290,3 +316,45 @@ func checkStatusCode(resp *http.Response, d debug) error {
return nil return nil
} }
type responseParser func(*http.Response) error
func newJSONParser(dst interface{}) responseParser {
return func(resp *http.Response) error {
return json.NewDecoder(resp.Body).Decode(dst)
}
}
func newTextParser(dst interface{}) responseParser {
return func(resp *http.Response) error {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if !bytes.Equal(b, []byte("ok")) {
return errors.New(string(b))
}
return nil
}
}
func newContentTypeParser(dst interface{}) responseParser {
return func(req *http.Response) (err error) {
var (
ctype string
)
if ctype, _, err = mime.ParseMediaType(req.Header.Get("Content-Type")); err != nil {
return err
}
switch ctype {
case "application/json":
return newJSONParser(dst)(req)
default:
return newTextParser(dst)(req)
}
}
}

View File

@ -41,6 +41,7 @@ type SearchMessage struct {
User string `json:"user"` User string `json:"user"`
Username string `json:"username"` Username string `json:"username"`
Timestamp string `json:"ts"` Timestamp string `json:"ts"`
Blocks Blocks `json:"blocks,omitempty"`
Text string `json:"text"` Text string `json:"text"`
Permalink string `json:"permalink"` Permalink string `json:"permalink"`
Attachments []Attachment `json:"attachments"` Attachments []Attachment `json:"attachments"`

View File

@ -51,6 +51,8 @@ type authTestResponseFull struct {
} }
// Client for the slack api. // Client for the slack api.
type ParamOption func(*url.Values)
type Client struct { type Client struct {
token string token string
endpoint string endpoint string

View File

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"net/url" "net/url"
"strconv" "strconv"
"time"
) )
const ( const (
@ -117,6 +118,7 @@ type User struct {
IsUltraRestricted bool `json:"is_ultra_restricted"` IsUltraRestricted bool `json:"is_ultra_restricted"`
IsStranger bool `json:"is_stranger"` IsStranger bool `json:"is_stranger"`
IsAppUser bool `json:"is_app_user"` IsAppUser bool `json:"is_app_user"`
IsInvitedUser bool `json:"is_invited_user"`
Has2FA bool `json:"has_2fa"` Has2FA bool `json:"has_2fa"`
HasFiles bool `json:"has_files"` HasFiles bool `json:"has_files"`
Presence string `json:"presence"` Presence string `json:"presence"`
@ -345,12 +347,19 @@ func (api *Client) GetUsers() ([]User, error) {
// GetUsersContext returns the list of users (with their detailed information) with a custom context // GetUsersContext returns the list of users (with their detailed information) with a custom context
func (api *Client) GetUsersContext(ctx context.Context) (results []User, err error) { func (api *Client) GetUsersContext(ctx context.Context) (results []User, err error) {
var ( p := api.GetUsersPaginated()
p UserPagination for err == nil {
) p, err = p.Next(ctx)
if err == nil {
for p = api.GetUsersPaginated(); !p.Done(err); p, err = p.Next(ctx) { results = append(results, p.Users...)
results = append(results, p.Users...) } else if rateLimitedError, ok := err.(*RateLimitedError); ok {
select {
case <-ctx.Done():
err = ctx.Err()
case <-time.After(rateLimitedError.RetryAfter):
err = nil
}
}
} }
return results, p.Failure(err) return results, p.Failure(err)
@ -411,13 +420,13 @@ func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) {
} }
// GetUserIdentityContext will retrieve user info available per identity scopes with a custom context // GetUserIdentityContext will retrieve user info available per identity scopes with a custom context
func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityResponse, error) { func (api *Client) GetUserIdentityContext(ctx context.Context) (response *UserIdentityResponse, err error) {
values := url.Values{ values := url.Values{
"token": {api.token}, "token": {api.token},
} }
response := &UserIdentityResponse{} response = &UserIdentityResponse{}
err := api.postMethod(ctx, "users.identity", values, response) err = api.postMethod(ctx, "users.identity", values, response)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -435,7 +444,7 @@ func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error {
} }
// SetUserPhotoContext changes the currently authenticated user's profile image using a custom context // SetUserPhotoContext changes the currently authenticated user's profile image using a custom context
func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) error { func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) (err error) {
response := &SlackResponse{} response := &SlackResponse{}
values := url.Values{ values := url.Values{
"token": {api.token}, "token": {api.token},
@ -450,7 +459,7 @@ func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params
values.Add("crop_w", strconv.Itoa(params.CropW)) values.Add("crop_w", strconv.Itoa(params.CropW))
} }
err := postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", values, response, api) err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", values, response, api)
if err != nil { if err != nil {
return err return err
} }
@ -464,13 +473,13 @@ func (api *Client) DeleteUserPhoto() error {
} }
// DeleteUserPhotoContext deletes the current authenticated user's profile image with a custom context // DeleteUserPhotoContext deletes the current authenticated user's profile image with a custom context
func (api *Client) DeleteUserPhotoContext(ctx context.Context) error { func (api *Client) DeleteUserPhotoContext(ctx context.Context) (err error) {
response := &SlackResponse{} response := &SlackResponse{}
values := url.Values{ values := url.Values{
"token": {api.token}, "token": {api.token},
} }
err := api.postMethod(ctx, "users.deletePhoto", values, response) err = api.postMethod(ctx, "users.deletePhoto", values, response)
if err != nil { if err != nil {
return err return err
} }
@ -484,13 +493,27 @@ func (api *Client) DeleteUserPhotoContext(ctx context.Context) error {
// the Slack API will unset the custom status/emoji. If statusExpiration is set to 0 // the Slack API will unset the custom status/emoji. If statusExpiration is set to 0
// the status will not expire. // the status will not expire.
func (api *Client) SetUserCustomStatus(statusText, statusEmoji string, statusExpiration int64) error { func (api *Client) SetUserCustomStatus(statusText, statusEmoji string, statusExpiration int64) error {
return api.SetUserCustomStatusContext(context.Background(), statusText, statusEmoji, statusExpiration) return api.SetUserCustomStatusContextWithUser(context.Background(), "", statusText, statusEmoji, statusExpiration)
} }
// SetUserCustomStatusContext will set a custom status and emoji for the currently authenticated user with a custom context // SetUserCustomStatusContext will set a custom status and emoji for the currently authenticated user with a custom context
// //
// For more information see SetUserCustomStatus // For more information see SetUserCustomStatus
func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, statusEmoji string, statusExpiration int64) error { func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, statusEmoji string, statusExpiration int64) error {
return api.SetUserCustomStatusContextWithUser(context.Background(), "", statusText, statusEmoji, statusExpiration)
}
// SetUserCustomStatusWithUser will set a custom status and emoji for the provided user.
//
// For more information see SetUserCustomStatus
func (api *Client) SetUserCustomStatusWithUser(user, statusText, statusEmoji string, statusExpiration int64) error {
return api.SetUserCustomStatusContextWithUser(context.Background(), user, statusText, statusEmoji, statusExpiration)
}
// SetUserCustomStatusContextWithUser will set a custom status and emoji for the provided user with a custom context
//
// For more information see SetUserCustomStatus
func (api *Client) SetUserCustomStatusContextWithUser(ctx context.Context, user, statusText, statusEmoji string, statusExpiration int64) error {
// XXX(theckman): this anonymous struct is for making requests to the Slack // 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 // 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. // these values we must provide a JSON document as the profile POST field.
@ -518,6 +541,7 @@ func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, s
} }
values := url.Values{ values := url.Values{
"user": {user},
"token": {api.token}, "token": {api.token},
"profile": {string(profile)}, "profile": {string(profile)},
} }

View File

@ -20,13 +20,17 @@ type WebhookMessage struct {
} }
func PostWebhook(url string, msg *WebhookMessage) error { func PostWebhook(url string, msg *WebhookMessage) error {
return PostWebhookCustomHTTP(url, http.DefaultClient, msg)
}
func PostWebhookCustomHTTP(url string, httpClient *http.Client, msg *WebhookMessage) error {
raw, err := json.Marshal(msg) raw, err := json.Marshal(msg)
if err != nil { if err != nil {
return errors.Wrap(err, "marshal failed") return errors.Wrap(err, "marshal failed")
} }
response, err := http.Post(url, "application/json", bytes.NewReader(raw)) response, err := httpClient.Post(url, "application/json", bytes.NewReader(raw))
if err != nil { if err != nil {
return errors.Wrap(err, "failed to post webhook") return errors.Wrap(err, "failed to post webhook")

View File

@ -78,9 +78,8 @@ func (rtm *RTM) Disconnect() error {
} }
// GetInfo returns the info structure received when calling // GetInfo returns the info structure received when calling
// "startrtm", holding all channels, groups and other metadata needed // "startrtm", holding metadata needed to implement a full
// to implement a full chat client. It will be non-nil after a call to // chat client. It will be non-nil after a call to StartRTM().
// StartRTM().
func (rtm *RTM) GetInfo() *Info { func (rtm *RTM) GetInfo() *Info {
return rtm.info return rtm.info
} }

View File

@ -35,6 +35,7 @@ type ConnectingEvent struct {
// DisconnectedEvent contains information about how we disconnected // DisconnectedEvent contains information about how we disconnected
type DisconnectedEvent struct { type DisconnectedEvent struct {
Intentional bool Intentional bool
Cause error
} }
// LatencyReport contains information about connection latency // LatencyReport contains information about connection latency

View File

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/nlopes/slack/internal/errorsx"
"github.com/nlopes/slack/internal/timex" "github.com/nlopes/slack/internal/timex"
) )
@ -138,21 +139,22 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke
ErrorObj: err, ErrorObj: err,
}} }}
// check if Disconnect() has been invoked. // get time we should wait before attempting to connect again
rtm.Debugf("reconnection %d failed: %s reconnecting in %v\n", boff.attempts, err, backoff)
// wait for one of the following to occur,
// backoff duration has elapsed, killChannel is signalled, or
// the rtm finishes disconnecting.
select { select {
case <-time.After(backoff): // retry after the backoff.
case intentional := <-rtm.killChannel: case intentional := <-rtm.killChannel:
if intentional { if intentional {
rtm.killConnection(intentional) rtm.killConnection(intentional, ErrRTMDisconnected)
return nil, nil, ErrRTMDisconnected return nil, nil, ErrRTMDisconnected
} }
case <-rtm.disconnected: case <-rtm.disconnected:
return nil, nil, ErrRTMDisconnected return nil, nil, ErrRTMDisconnected
default:
} }
// get time we should wait before attempting to connect again
rtm.Debugf("reconnection %d failed: %s reconnecting in %v\n", boff.attempts, err, backoff)
time.Sleep(backoff)
} }
} }
@ -205,14 +207,14 @@ func (rtm *RTM) startRTMAndDial(useRTMStart bool) (info *Info, _ *websocket.Conn
// //
// This should not be called directly! Instead a boolean value (true for // This should not be called directly! Instead a boolean value (true for
// intentional, false otherwise) should be sent to the killChannel on the RTM. // intentional, false otherwise) should be sent to the killChannel on the RTM.
func (rtm *RTM) killConnection(intentional bool) (err error) { func (rtm *RTM) killConnection(intentional bool, cause error) (err error) {
rtm.Debugln("killing connection") rtm.Debugln("killing connection")
if rtm.conn != nil { if rtm.conn != nil {
err = rtm.conn.Close() err = rtm.conn.Close()
} }
rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{intentional}} rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: intentional, Cause: cause}}
if intentional { if intentional {
rtm.disconnect() rtm.disconnect()
@ -233,22 +235,21 @@ func (rtm *RTM) handleEvents() {
select { select {
// catch "stop" signal on channel close // catch "stop" signal on channel close
case intentional := <-rtm.killChannel: case intentional := <-rtm.killChannel:
_ = rtm.killConnection(intentional) _ = rtm.killConnection(intentional, errorsx.String("signaled"))
return return
// detect when the connection is dead. // detect when the connection is dead.
case <-rtm.pingDeadman.C: case <-rtm.pingDeadman.C:
rtm.Debugln("deadman switch trigger disconnecting") _ = rtm.killConnection(false, errorsx.String("deadman switch triggered"))
_ = rtm.killConnection(false)
return return
// send pings on ticker interval // send pings on ticker interval
case <-ticker.C: case <-ticker.C:
if err := rtm.ping(); err != nil { if err := rtm.ping(); err != nil {
_ = rtm.killConnection(false) _ = rtm.killConnection(false, err)
return return
} }
case <-rtm.forcePing: case <-rtm.forcePing:
if err := rtm.ping(); err != nil { if err := rtm.ping(); err != nil {
_ = rtm.killConnection(false) _ = rtm.killConnection(false, err)
return return
} }
// listen for messages that need to be sent // listen for messages that need to be sent
@ -258,7 +259,7 @@ func (rtm *RTM) handleEvents() {
case rawEvent := <-rtm.rawEvents: case rawEvent := <-rtm.rawEvents:
switch rtm.handleRawEvent(rawEvent) { switch rtm.handleRawEvent(rawEvent) {
case rtmEventTypeGoodbye: case rtmEventTypeGoodbye:
_ = rtm.killConnection(false) _ = rtm.killConnection(false, errorsx.String("goodbye detected"))
return return
default: default:
} }
@ -310,7 +311,6 @@ func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) {
Message: msg, Message: msg,
ErrorObj: err, ErrorObj: err,
}} }}
// TODO force ping?
} }
} }

4
vendor/modules.txt vendored
View File

@ -2,7 +2,7 @@
github.com/0xAX/notificator github.com/0xAX/notificator
# github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04 # github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04
github.com/erroneousboat/termui github.com/erroneousboat/termui
# github.com/gorilla/websocket v1.4.0 # github.com/gorilla/websocket v1.4.1
github.com/gorilla/websocket github.com/gorilla/websocket
# github.com/maruel/panicparse v1.1.1 # github.com/maruel/panicparse v1.1.1
github.com/maruel/panicparse/stack github.com/maruel/panicparse/stack
@ -10,7 +10,7 @@ github.com/maruel/panicparse/stack
github.com/mattn/go-runewidth github.com/mattn/go-runewidth
# github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 # github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7
github.com/mitchellh/go-wordwrap github.com/mitchellh/go-wordwrap
# github.com/nlopes/slack v0.5.1-0.20190515005541-e2954b1409b0 # github.com/nlopes/slack v0.6.0
github.com/nlopes/slack github.com/nlopes/slack
github.com/nlopes/slack/internal/errorsx github.com/nlopes/slack/internal/errorsx
github.com/nlopes/slack/internal/timex github.com/nlopes/slack/internal/timex