From 7d87ccef354915750908987f1edd0a33c595921b Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Fri, 23 Mar 2018 11:16:06 +0100 Subject: [PATCH 01/12] Migrate to dep and update dependencies Fixes #98 --- Gopkg.lock | 56 + Gopkg.toml | 50 + .../erroneousboat/termui/.gitignore | 26 + .../erroneousboat/termui/.travis.yml | 6 + .../erroneousboat/termui/barchart.go | 2 +- .../github.com/erroneousboat/termui/events.go | 11 +- .../github.com/erroneousboat/termui/grid.go | 4 +- .../github.com/erroneousboat/termui/helper.go | 2 +- .../erroneousboat/termui/linechart.go | 4 +- .../github.com/erroneousboat/termui/list.go | 2 +- .../erroneousboat/termui/mbarchart.go | 8 +- .../github.com/erroneousboat/termui/render.go | 4 +- .../erroneousboat/termui/sparkline.go | 2 +- .../github.com/gorilla/websocket/.gitignore | 25 + .../github.com/gorilla/websocket/.travis.yml | 19 + vendor/github.com/gorilla/websocket/AUTHORS | 8 + vendor/github.com/gorilla/websocket/LICENSE | 22 + vendor/github.com/gorilla/websocket/README.md | 64 + vendor/github.com/gorilla/websocket/client.go | 392 ++++++ .../gorilla/websocket/client_clone.go | 16 + .../gorilla/websocket/client_clone_legacy.go | 38 + .../gorilla/websocket/compression.go | 148 +++ vendor/github.com/gorilla/websocket/conn.go | 1149 +++++++++++++++++ .../github.com/gorilla/websocket/conn_read.go | 18 + .../gorilla/websocket/conn_read_legacy.go | 21 + vendor/github.com/gorilla/websocket/doc.go | 180 +++ vendor/github.com/gorilla/websocket/json.go | 55 + vendor/github.com/gorilla/websocket/mask.go | 55 + .../github.com/gorilla/websocket/mask_safe.go | 15 + .../github.com/gorilla/websocket/prepared.go | 103 ++ vendor/github.com/gorilla/websocket/server.go | 291 +++++ vendor/github.com/gorilla/websocket/util.go | 214 +++ .../maruel/panicparse/stack/source.go | 37 +- .../maruel/panicparse/stack/stack.go | 12 +- .../github.com/mattn/go-runewidth/.travis.yml | 8 + .../mattn/go-runewidth/runewidth.go | 1 - vendor/github.com/nlopes/slack/.gitignore | 2 + vendor/github.com/nlopes/slack/.travis.yml | 21 + vendor/github.com/nlopes/slack/README.md | 2 + vendor/github.com/nlopes/slack/admin.go | 36 +- vendor/github.com/nlopes/slack/attachments.go | 6 + vendor/github.com/nlopes/slack/bots.go | 9 +- vendor/github.com/nlopes/slack/channels.go | 242 ++-- vendor/github.com/nlopes/slack/chat.go | 106 +- .../github.com/nlopes/slack/conversation.go | 555 ++++++++ vendor/github.com/nlopes/slack/dnd.go | 28 +- vendor/github.com/nlopes/slack/emoji.go | 5 +- vendor/github.com/nlopes/slack/files.go | 65 +- vendor/github.com/nlopes/slack/groups.go | 106 +- vendor/github.com/nlopes/slack/im.go | 29 +- vendor/github.com/nlopes/slack/logger.go | 53 + vendor/github.com/nlopes/slack/messages.go | 18 +- vendor/github.com/nlopes/slack/misc.go | 89 +- vendor/github.com/nlopes/slack/oauth.go | 2 +- vendor/github.com/nlopes/slack/pins.go | 15 +- vendor/github.com/nlopes/slack/reactions.go | 20 +- vendor/github.com/nlopes/slack/rtm.go | 42 +- vendor/github.com/nlopes/slack/search.go | 5 +- vendor/github.com/nlopes/slack/slack.go | 78 +- vendor/github.com/nlopes/slack/slash.go | 49 + vendor/github.com/nlopes/slack/stars.go | 15 +- vendor/github.com/nlopes/slack/team.go | 29 +- vendor/github.com/nlopes/slack/usergroups.go | 32 +- vendor/github.com/nlopes/slack/users.go | 118 +- vendor/github.com/nlopes/slack/websocket.go | 8 +- .../nlopes/slack/websocket_internals.go | 7 + .../nlopes/slack/websocket_managed_conn.go | 41 +- .../nlopes/slack/websocket_proxy.go | 83 -- .../nlopes/slack/websocket_utils.go | 20 - vendor/github.com/nsf/termbox-go/README.md | 6 +- vendor/github.com/nsf/termbox-go/api.go | 44 +- .../github.com/nsf/termbox-go/api_common.go | 2 +- vendor/github.com/nsf/termbox-go/escwait.go | 11 + .../nsf/termbox-go/escwait_darwin.go | 9 + vendor/github.com/nsf/termbox-go/syscalls.go | 39 + vendor/github.com/nsf/termbox-go/termbox.go | 40 +- vendor/github.com/nsf/termbox-go/terminfo.go | 7 +- .../renstrom/fuzzysearch/fuzzy/fuzzy.go | 16 +- vendor/golang.org/x/net/LICENSE | 27 - vendor/golang.org/x/net/PATENTS | 22 - vendor/golang.org/x/net/websocket/client.go | 106 -- vendor/golang.org/x/net/websocket/dial.go | 24 - vendor/golang.org/x/net/websocket/hybi.go | 583 --------- vendor/golang.org/x/net/websocket/server.go | 113 -- .../golang.org/x/net/websocket/websocket.go | 448 ------- vendor/vendor.json | 243 ---- 86 files changed, 4564 insertions(+), 2180 deletions(-) create mode 100644 Gopkg.lock create mode 100644 Gopkg.toml create mode 100644 vendor/github.com/erroneousboat/termui/.gitignore create mode 100644 vendor/github.com/erroneousboat/termui/.travis.yml create mode 100644 vendor/github.com/gorilla/websocket/.gitignore create mode 100644 vendor/github.com/gorilla/websocket/.travis.yml create mode 100644 vendor/github.com/gorilla/websocket/AUTHORS create mode 100644 vendor/github.com/gorilla/websocket/LICENSE create mode 100644 vendor/github.com/gorilla/websocket/README.md create mode 100644 vendor/github.com/gorilla/websocket/client.go create mode 100644 vendor/github.com/gorilla/websocket/client_clone.go create mode 100644 vendor/github.com/gorilla/websocket/client_clone_legacy.go create mode 100644 vendor/github.com/gorilla/websocket/compression.go create mode 100644 vendor/github.com/gorilla/websocket/conn.go create mode 100644 vendor/github.com/gorilla/websocket/conn_read.go create mode 100644 vendor/github.com/gorilla/websocket/conn_read_legacy.go create mode 100644 vendor/github.com/gorilla/websocket/doc.go create mode 100644 vendor/github.com/gorilla/websocket/json.go create mode 100644 vendor/github.com/gorilla/websocket/mask.go create mode 100644 vendor/github.com/gorilla/websocket/mask_safe.go create mode 100644 vendor/github.com/gorilla/websocket/prepared.go create mode 100644 vendor/github.com/gorilla/websocket/server.go create mode 100644 vendor/github.com/gorilla/websocket/util.go create mode 100644 vendor/github.com/mattn/go-runewidth/.travis.yml create mode 100644 vendor/github.com/nlopes/slack/.gitignore create mode 100644 vendor/github.com/nlopes/slack/.travis.yml create mode 100644 vendor/github.com/nlopes/slack/logger.go create mode 100644 vendor/github.com/nlopes/slack/slash.go delete mode 100644 vendor/github.com/nlopes/slack/websocket_proxy.go delete mode 100644 vendor/github.com/nlopes/slack/websocket_utils.go create mode 100644 vendor/github.com/nsf/termbox-go/escwait.go create mode 100644 vendor/github.com/nsf/termbox-go/escwait_darwin.go create mode 100644 vendor/github.com/nsf/termbox-go/syscalls.go delete mode 100644 vendor/golang.org/x/net/LICENSE delete mode 100644 vendor/golang.org/x/net/PATENTS delete mode 100644 vendor/golang.org/x/net/websocket/client.go delete mode 100644 vendor/golang.org/x/net/websocket/dial.go delete mode 100644 vendor/golang.org/x/net/websocket/hybi.go delete mode 100644 vendor/golang.org/x/net/websocket/server.go delete mode 100644 vendor/golang.org/x/net/websocket/websocket.go delete mode 100644 vendor/vendor.json diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..84227fa --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,56 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/erroneousboat/termui" + packages = ["."] + revision = "24acd523c756fd9728824cdfac66aad9d8982fb7" + version = "v2.2.0" + +[[projects]] + name = "github.com/gorilla/websocket" + packages = ["."] + revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" + version = "v1.2.0" + +[[projects]] + name = "github.com/maruel/panicparse" + packages = ["stack"] + revision = "ad661195ed0e88491e0f14be6613304e3b1141d6" + +[[projects]] + name = "github.com/mattn/go-runewidth" + packages = ["."] + revision = "9e777a8366cce605130a531d2cd6363d07ad7317" + version = "v0.0.2" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/go-wordwrap" + packages = ["."] + revision = "ad45545899c7b13c020ea92b2072220eefad42b8" + +[[projects]] + name = "github.com/nlopes/slack" + packages = ["."] + revision = "8ab4d0b364ef1e9af5d102531da20d5ec902b6c4" + version = "v0.2.0" + +[[projects]] + branch = "master" + name = "github.com/nsf/termbox-go" + packages = ["."] + revision = "e2050e41c8847748ec5288741c0b19a8cb26d084" + +[[projects]] + name = "github.com/renstrom/fuzzysearch" + packages = ["fuzzy"] + revision = "d4ca9dfccd55dc6b076f9880d49c35315922c1f4" + version = "v1.0.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "719f2440551009ce6f1e1a7f6e56db762acdadd9a0e05245ee427b7455e50233" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..1d9ea71 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,50 @@ +# 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" + version = "2.2.0" + +[[constraint]] + name = "github.com/mattn/go-runewidth" + version = "0.0.2" + +[[constraint]] + name = "github.com/nlopes/slack" + version = "0.2.0" + +[[constraint]] + branch = "master" + name = "github.com/nsf/termbox-go" + +[[constraint]] + name = "github.com/renstrom/fuzzysearch" + version = "1.0.0" + +[prune] + go-tests = true + unused-packages = true diff --git a/vendor/github.com/erroneousboat/termui/.gitignore b/vendor/github.com/erroneousboat/termui/.gitignore new file mode 100644 index 0000000..8b156b0 --- /dev/null +++ b/vendor/github.com/erroneousboat/termui/.gitignore @@ -0,0 +1,26 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof +.DS_Store +/vendor diff --git a/vendor/github.com/erroneousboat/termui/.travis.yml b/vendor/github.com/erroneousboat/termui/.travis.yml new file mode 100644 index 0000000..206e887 --- /dev/null +++ b/vendor/github.com/erroneousboat/termui/.travis.yml @@ -0,0 +1,6 @@ +language: go + +go: + - tip + +script: go test -v ./ \ No newline at end of file diff --git a/vendor/github.com/erroneousboat/termui/barchart.go b/vendor/github.com/erroneousboat/termui/barchart.go index d960797..6560c8b 100644 --- a/vendor/github.com/erroneousboat/termui/barchart.go +++ b/vendor/github.com/erroneousboat/termui/barchart.go @@ -62,7 +62,7 @@ func (bc *BarChart) layout() { } //bc.max = bc.Data[0] // what if Data is nil? Sometimes when bar graph is nill it produces panic with panic: runtime error: index out of range - // Assign a negative value to get maxvalue auto-populates + // Asign a negative value to get maxvalue auto-populates if bc.max == 0 { bc.max = -1 } diff --git a/vendor/github.com/erroneousboat/termui/events.go b/vendor/github.com/erroneousboat/termui/events.go index f0a45da..eb7319b 100644 --- a/vendor/github.com/erroneousboat/termui/events.go +++ b/vendor/github.com/erroneousboat/termui/events.go @@ -126,7 +126,7 @@ func hookTermboxEvt() { e := termbox.PollEvent() for _, c := range sysEvtChs { - func(ch chan Event) { + go func(ch chan Event) { ch <- crtTermboxEvt(e) }(c) } @@ -221,7 +221,6 @@ func findMatch(mux map[string]func(Event), path string) string { return pattern } - // Remove all existing defined Handlers from the map func (es *EvtStream) ResetHandlers() { for Path, _ := range es.Handlers { @@ -244,7 +243,7 @@ func (es *EvtStream) Loop() { case "/sig/stoploop": return } - func(a Event) { + go func(a Event) { es.RLock() defer es.RUnlock() if pattern := es.match(a.Path); pattern != "" { @@ -274,10 +273,6 @@ func Handle(path string, handler func(Event)) { DefaultEvtStream.Handle(path, handler) } -func ResetHandlers() { - DefaultEvtStream.ResetHandlers() -} - func Loop() { DefaultEvtStream.Loop() } @@ -314,7 +309,7 @@ func NewTimerCh(du time.Duration) chan Event { return t } -var DefaultHandler = func(e Event) { +var DefualtHandler = func(e Event) { } var usrEvtCh = make(chan Event) diff --git a/vendor/github.com/erroneousboat/termui/grid.go b/vendor/github.com/erroneousboat/termui/grid.go index 851489d..a950232 100644 --- a/vendor/github.com/erroneousboat/termui/grid.go +++ b/vendor/github.com/erroneousboat/termui/grid.go @@ -48,7 +48,7 @@ func (r *Row) assignWidth(w int) { accW := 0 // acc span and offset calcW := make([]int, len(r.Cols)) // calculated width - calcOftX := make([]int, len(r.Cols)) // computed start position of x + calcOftX := make([]int, len(r.Cols)) // computated start position of x for i, c := range r.Cols { accW += c.Span + c.Offset @@ -266,7 +266,7 @@ func (g *Grid) Align() { } } -// Buffer implements Bufferer interface. +// Buffer implments Bufferer interface. func (g Grid) Buffer() Buffer { buf := NewBuffer() diff --git a/vendor/github.com/erroneousboat/termui/helper.go b/vendor/github.com/erroneousboat/termui/helper.go index 5a71afb..18a6770 100644 --- a/vendor/github.com/erroneousboat/termui/helper.go +++ b/vendor/github.com/erroneousboat/termui/helper.go @@ -100,7 +100,7 @@ func charWidth(ch rune) int { var whiteSpaceRegex = regexp.MustCompile(`\s`) -// StringToAttribute converts text to a termui attribute. You may specify more +// StringToAttribute converts text to a termui attribute. You may specifiy more // then one attribute like that: "BLACK, BOLD, ...". All whitespaces // are ignored. func StringToAttribute(text string) Attribute { diff --git a/vendor/github.com/erroneousboat/termui/linechart.go b/vendor/github.com/erroneousboat/termui/linechart.go index 84e7e28..f7eea28 100644 --- a/vendor/github.com/erroneousboat/termui/linechart.go +++ b/vendor/github.com/erroneousboat/termui/linechart.go @@ -35,7 +35,7 @@ var braillePatterns = map[[2]int]rune{ var lSingleBraille = [4]rune{'\u2840', '⠄', '⠂', '⠁'} var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'} -// LineChart has two modes: braille(default) and dot. Using braille gives 2x capacity as dot mode, +// LineChart has two modes: braille(default) and dot. Using braille gives 2x capicity as dot mode, // because one braille char can represent two data points. /* lc := termui.NewLineChart() @@ -87,7 +87,7 @@ func NewLineChart() *LineChart { } // one cell contains two data points -// so the capacity is 2x as dot-mode +// so the capicity is 2x as dot-mode func (lc *LineChart) renderBraille() Buffer { buf := NewBuffer() diff --git a/vendor/github.com/erroneousboat/termui/list.go b/vendor/github.com/erroneousboat/termui/list.go index 5a59215..ea6635e 100644 --- a/vendor/github.com/erroneousboat/termui/list.go +++ b/vendor/github.com/erroneousboat/termui/list.go @@ -14,7 +14,7 @@ import "strings" strs := []string{ "[0] github.com/gizak/termui", "[1] editbox.go", - "[2] interrupt.go", + "[2] iterrupt.go", "[3] keyboard.go", "[4] output.go", "[5] random_out.go", diff --git a/vendor/github.com/erroneousboat/termui/mbarchart.go b/vendor/github.com/erroneousboat/termui/mbarchart.go index 0ce6c45..0f91e97 100644 --- a/vendor/github.com/erroneousboat/termui/mbarchart.go +++ b/vendor/github.com/erroneousboat/termui/mbarchart.go @@ -8,7 +8,7 @@ import ( "fmt" ) -// This is the implementation of multi-colored or stacked bar graph. This is different from default barGraph which is implemented in bar.go +// This is the implemetation of multi-colored or stacked bar graph. This is different from default barGraph which is implemented in bar.go // Multi-Colored-BarChart creates multiple bars in a widget: /* bc := termui.NewMBarChart() @@ -72,7 +72,7 @@ func (bc *MBarChart) layout() { } bc.numStack = DataLen - //We need to know what is the minimum size of data array data[0] could have 10 elements data[1] could have only 5, so we plot only 5 bar graphs + //We need to know what is the mimimum size of data array data[0] could have 10 elements data[1] could have only 5, so we plot only 5 bar graphs for i := 0; i < DataLen; i++ { if bc.minDataLen > len(bc.Data[i]) { @@ -90,13 +90,13 @@ func (bc *MBarChart) layout() { for i := 0; i < bc.numStack; i++ { bc.dataNum[i] = make([][]rune, len(bc.Data[i])) - //For each stack of bar calculate the rune + //For each stack of bar calcualte the rune for j := 0; j < LabelLen && i < bc.numBar; j++ { n := bc.Data[i][j] s := fmt.Sprint(n) bc.dataNum[i][j] = trimStr2Runes(s, bc.BarWidth) } - //If color is not defined by default then populate a color that is different from the previous bar + //If color is not defined by default then populate a color that is different from the prevous bar if bc.BarColor[i] == ColorDefault && bc.NumColor[i] == ColorDefault { if i == 0 { bc.BarColor[i] = ColorBlack diff --git a/vendor/github.com/erroneousboat/termui/render.go b/vendor/github.com/erroneousboat/termui/render.go index 4959c2a..5b58409 100644 --- a/vendor/github.com/erroneousboat/termui/render.go +++ b/vendor/github.com/erroneousboat/termui/render.go @@ -35,7 +35,7 @@ func Init() error { } sysEvtChs = make([]chan Event, 0) - // go hookTermboxEvt() + go hookTermboxEvt() renderJobs = make(chan []Bufferer) //renderLock = new(sync.RWMutex) @@ -51,7 +51,7 @@ func Init() error { DefaultEvtStream.Merge("timer", NewTimerCh(time.Second)) DefaultEvtStream.Merge("custom", usrEvtCh) - DefaultEvtStream.Handle("/", DefaultHandler) + DefaultEvtStream.Handle("/", DefualtHandler) DefaultEvtStream.Handle("/sys/wnd/resize", func(e Event) { w := e.Data.(EvtWnd) Body.Width = w.Width diff --git a/vendor/github.com/erroneousboat/termui/sparkline.go b/vendor/github.com/erroneousboat/termui/sparkline.go index 75e7c52..d906e49 100644 --- a/vendor/github.com/erroneousboat/termui/sparkline.go +++ b/vendor/github.com/erroneousboat/termui/sparkline.go @@ -51,7 +51,7 @@ func NewSparkline() Sparkline { LineColor: ThemeAttr("sparkline.line.fg")} } -// NewSparklines return a new *Sparklines with given Sparkline(s), you can always add a new Sparkline later. +// NewSparklines return a new *Spaklines with given Sparkline(s), you can always add a new Sparkline later. func NewSparklines(ss ...Sparkline) *Sparklines { s := &Sparklines{Block: *NewBlock(), Lines: ss} return s diff --git a/vendor/github.com/gorilla/websocket/.gitignore b/vendor/github.com/gorilla/websocket/.gitignore new file mode 100644 index 0000000..ac71020 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/.gitignore @@ -0,0 +1,25 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe + +.idea/ +*.iml \ No newline at end of file diff --git a/vendor/github.com/gorilla/websocket/.travis.yml b/vendor/github.com/gorilla/websocket/.travis.yml new file mode 100644 index 0000000..3d8d29c --- /dev/null +++ b/vendor/github.com/gorilla/websocket/.travis.yml @@ -0,0 +1,19 @@ +language: go +sudo: false + +matrix: + include: + - go: 1.4 + - go: 1.5 + - go: 1.6 + - go: 1.7 + - go: 1.8 + - 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 ./... diff --git a/vendor/github.com/gorilla/websocket/AUTHORS b/vendor/github.com/gorilla/websocket/AUTHORS new file mode 100644 index 0000000..b003eca --- /dev/null +++ b/vendor/github.com/gorilla/websocket/AUTHORS @@ -0,0 +1,8 @@ +# This is the official list of Gorilla WebSocket authors for copyright +# purposes. +# +# Please keep the list sorted. + +Gary Burd +Joachim Bauch + diff --git a/vendor/github.com/gorilla/websocket/LICENSE b/vendor/github.com/gorilla/websocket/LICENSE new file mode 100644 index 0000000..9171c97 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/websocket/README.md b/vendor/github.com/gorilla/websocket/README.md new file mode 100644 index 0000000..33c3d2b --- /dev/null +++ b/vendor/github.com/gorilla/websocket/README.md @@ -0,0 +1,64 @@ +# Gorilla WebSocket + +Gorilla WebSocket is a [Go](http://golang.org/) implementation of the +[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 + +* [API Reference](http://godoc.org/github.com/gorilla/websocket) +* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat) +* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command) +* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo) +* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch) + +### Status + +The Gorilla WebSocket package provides a complete and tested implementation of +the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The +package API is stable. + +### Installation + + go get github.com/gorilla/websocket + +### Protocol Compliance + +The Gorilla WebSocket package passes the server tests in the [Autobahn Test +Suite](http://autobahn.ws/testsuite) using the application in the [examples/autobahn +subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn). + +### Gorilla WebSocket compared with other packages + + + + + + + + + + + + + + + + + + +
github.com/gorillagolang.org/x/net
RFC 6455 Features
Passes Autobahn Test SuiteYesNo
Receive fragmented messageYesNo, see note 1
Send close messageYesNo
Send pings and receive pongsYesNo
Get the type of a received data messageYesYes, see note 2
Other Features
Compression ExtensionsExperimentalNo
Read message using io.ReaderYesNo, see note 3
Write message using io.WriteCloserYesNo, see note 3
+ +Notes: + +1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html). +2. The application can get the type of a received data message by implementing + a [Codec marshal](http://godoc.org/golang.org/x/net/websocket#Codec.Marshal) + function. +3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries. + Read returns when the input buffer is full or a frame boundary is + encountered. Each call to Write sends a single frame message. The Gorilla + io.Reader and io.WriteCloser operate on a single WebSocket message. + diff --git a/vendor/github.com/gorilla/websocket/client.go b/vendor/github.com/gorilla/websocket/client.go new file mode 100644 index 0000000..43a87c7 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/client.go @@ -0,0 +1,392 @@ +// Copyright 2013 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 ( + "bufio" + "bytes" + "crypto/tls" + "encoding/base64" + "errors" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +// ErrBadHandshake is returned when the server response to opening handshake is +// invalid. +var ErrBadHandshake = errors.New("websocket: bad handshake") + +var errInvalidCompression = errors.New("websocket: invalid compression negotiation") + +// NewClient creates a new client connection using the given net connection. +// The URL u specifies the host and request URI. 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). +// +// If the WebSocket handshake fails, ErrBadHandshake is returned along with a +// non-nil *http.Response so that callers can handle redirects, authentication, +// etc. +// +// Deprecated: Use Dialer instead. +func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) { + d := Dialer{ + ReadBufferSize: readBufSize, + WriteBufferSize: writeBufSize, + NetDial: func(net, addr string) (net.Conn, error) { + return netConn, nil + }, + } + return d.Dial(u.String(), requestHeader) +} + +// A Dialer contains options for connecting to WebSocket server. +type Dialer struct { + // NetDial specifies the dial function for creating TCP connections. If + // NetDial is nil, net.Dial is used. + NetDial func(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. + // If Proxy is nil or returns a nil *URL, no proxy is used. + Proxy func(*http.Request) (*url.URL, error) + + // TLSClientConfig specifies the TLS configuration to use with tls.Client. + // If nil, the default configuration is used. + TLSClientConfig *tls.Config + + // HandshakeTimeout specifies the duration for the handshake to complete. + HandshakeTimeout time.Duration + + // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer + // 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. + ReadBufferSize, WriteBufferSize int + + // Subprotocols specifies the client's requested subprotocols. + Subprotocols []string + + // EnableCompression specifies if the client should attempt to negotiate + // per message compression (RFC 7692). Setting this value to true does not + // guarantee that compression will be supported. Currently only "no context + // takeover" modes are supported. + EnableCompression bool + + // Jar specifies the cookie jar. + // If Jar is nil, cookies are not sent in requests and ignored + // in responses. + Jar http.CookieJar +} + +var errMalformedURL = errors.New("malformed ws or wss URL") + +// parseURL parses the URL. +// +// This function is a replacement for the standard library url.Parse function. +// In Go 1.4 and earlier, url.Parse loses information from the path. +func parseURL(s string) (*url.URL, error) { + // From the RFC: + // + // ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ] + // wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ] + var u url.URL + switch { + case strings.HasPrefix(s, "ws://"): + u.Scheme = "ws" + s = s[len("ws://"):] + case strings.HasPrefix(s, "wss://"): + u.Scheme = "wss" + s = s[len("wss://"):] + default: + return nil, errMalformedURL + } + + if i := strings.Index(s, "?"); i >= 0 { + u.RawQuery = s[i+1:] + s = s[:i] + } + + if i := strings.Index(s, "/"); i >= 0 { + u.Opaque = s[i:] + s = s[:i] + } else { + u.Opaque = "/" + } + + u.Host = s + + if strings.Contains(u.Host, "@") { + // Don't bother parsing user information because user information is + // not allowed in websocket URIs. + return nil, errMalformedURL + } + + return &u, nil +} + +func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { + hostPort = u.Host + hostNoPort = u.Host + if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") { + hostNoPort = hostNoPort[:i] + } else { + switch u.Scheme { + case "wss": + hostPort += ":443" + case "https": + hostPort += ":443" + default: + hostPort += ":80" + } + } + return hostPort, hostNoPort +} + +// DefaultDialer is a dialer with all fields set to the default zero values. +var DefaultDialer = &Dialer{ + Proxy: http.ProxyFromEnvironment, +} + +// Dial 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). +// +// 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) { + + if d == nil { + d = &Dialer{ + Proxy: http.ProxyFromEnvironment, + } + } + + challengeKey, err := generateChallengeKey() + if err != nil { + return nil, nil, err + } + + u, err := parseURL(urlStr) + if err != nil { + return nil, nil, err + } + + switch u.Scheme { + case "ws": + u.Scheme = "http" + case "wss": + u.Scheme = "https" + default: + return nil, nil, errMalformedURL + } + + if u.User != nil { + // User name and password are not allowed in websocket URIs. + return nil, nil, errMalformedURL + } + + req := &http.Request{ + Method: "GET", + URL: u, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Host: u.Host, + } + + // Set the cookies present in the cookie jar of the dialer + if d.Jar != nil { + for _, cookie := range d.Jar.Cookies(u) { + req.AddCookie(cookie) + } + } + + // Set the request headers using the capitalization for names and values in + // RFC examples. Although the capitalization shouldn't matter, there are + // servers that depend on it. The Header.Set method is not used because the + // method canonicalizes the header names. + req.Header["Upgrade"] = []string{"websocket"} + req.Header["Connection"] = []string{"Upgrade"} + req.Header["Sec-WebSocket-Key"] = []string{challengeKey} + req.Header["Sec-WebSocket-Version"] = []string{"13"} + if len(d.Subprotocols) > 0 { + req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")} + } + for k, vs := range requestHeader { + switch { + case k == "Host": + if len(vs) > 0 { + req.Host = vs[0] + } + case k == "Upgrade" || + k == "Connection" || + k == "Sec-Websocket-Key" || + k == "Sec-Websocket-Version" || + k == "Sec-Websocket-Extensions" || + (k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0): + return nil, nil, errors.New("websocket: duplicate header not allowed: " + k) + default: + req.Header[k] = vs + } + } + + if d.EnableCompression { + req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover") + } + + hostPort, hostNoPort := hostPortNoPort(u) + + var proxyURL *url.URL + // Check wether the proxy method has been configured + if d.Proxy != nil { + proxyURL, err = d.Proxy(req) + } + if err != nil { + return nil, nil, err + } + + var targetHostPort string + if proxyURL != nil { + targetHostPort, _ = hostPortNoPort(proxyURL) + } else { + targetHostPort = hostPort + } + + var deadline time.Time + if d.HandshakeTimeout != 0 { + deadline = time.Now().Add(d.HandshakeTimeout) + } + + netDial := d.NetDial + if netDial == nil { + netDialer := &net.Dialer{Deadline: deadline} + netDial = netDialer.Dial + } + + netConn, err := netDial("tcp", targetHostPort) + if err != nil { + return nil, nil, err + } + + defer func() { + if netConn != nil { + netConn.Close() + } + }() + + if err := netConn.SetDeadline(deadline); err != nil { + return nil, nil, err + } + + if proxyURL != nil { + connectHeader := make(http.Header) + if user := proxyURL.User; user != nil { + proxyUser := user.Username() + if proxyPassword, passwordSet := user.Password(); passwordSet { + credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword)) + connectHeader.Set("Proxy-Authorization", "Basic "+credential) + } + } + connectReq := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: hostPort}, + Host: hostPort, + Header: connectHeader, + } + + connectReq.Write(netConn) + + // Read response. + // Okay to use and discard buffered reader here, because + // TLS server will not speak until spoken to. + br := bufio.NewReader(netConn) + resp, err := http.ReadResponse(br, connectReq) + if err != nil { + return nil, nil, err + } + if resp.StatusCode != 200 { + f := strings.SplitN(resp.Status, " ", 2) + return nil, nil, errors.New(f[1]) + } + } + + if u.Scheme == "https" { + cfg := cloneTLSConfig(d.TLSClientConfig) + if cfg.ServerName == "" { + cfg.ServerName = hostNoPort + } + tlsConn := tls.Client(netConn, cfg) + netConn = tlsConn + if err := tlsConn.Handshake(); err != nil { + return nil, nil, err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return nil, nil, err + } + } + } + + conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize) + + if err := req.Write(netConn); err != nil { + return nil, nil, err + } + + resp, err := http.ReadResponse(conn.br, req) + if err != nil { + return nil, nil, err + } + + if d.Jar != nil { + if rc := resp.Cookies(); len(rc) > 0 { + d.Jar.SetCookies(u, rc) + } + } + + if resp.StatusCode != 101 || + !strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") || + !strings.EqualFold(resp.Header.Get("Connection"), "upgrade") || + resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) { + // Before closing the network connection on return from this + // function, slurp up some of the response to aid application + // debugging. + buf := make([]byte, 1024) + n, _ := io.ReadFull(resp.Body, buf) + resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n])) + return nil, resp, ErrBadHandshake + } + + for _, ext := range parseExtensions(resp.Header) { + if ext[""] != "permessage-deflate" { + continue + } + _, snct := ext["server_no_context_takeover"] + _, cnct := ext["client_no_context_takeover"] + if !snct || !cnct { + return nil, resp, errInvalidCompression + } + conn.newCompressionWriter = compressNoContextTakeover + conn.newDecompressionReader = decompressNoContextTakeover + break + } + + resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) + conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol") + + netConn.SetDeadline(time.Time{}) + netConn = nil // to avoid close in defer. + return conn, resp, nil +} diff --git a/vendor/github.com/gorilla/websocket/client_clone.go b/vendor/github.com/gorilla/websocket/client_clone.go new file mode 100644 index 0000000..4f0d943 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/client_clone.go @@ -0,0 +1,16 @@ +// Copyright 2013 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.8 + +package websocket + +import "crypto/tls" + +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + if cfg == nil { + return &tls.Config{} + } + return cfg.Clone() +} diff --git a/vendor/github.com/gorilla/websocket/client_clone_legacy.go b/vendor/github.com/gorilla/websocket/client_clone_legacy.go new file mode 100644 index 0000000..babb007 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/client_clone_legacy.go @@ -0,0 +1,38 @@ +// Copyright 2013 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.8 + +package websocket + +import "crypto/tls" + +// cloneTLSConfig clones all public fields except the fields +// SessionTicketsDisabled and SessionTicketKey. This avoids copying the +// sync.Mutex in the sync.Once and makes it safe to call cloneTLSConfig on a +// config in active use. +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + if cfg == nil { + return &tls.Config{} + } + return &tls.Config{ + Rand: cfg.Rand, + Time: cfg.Time, + Certificates: cfg.Certificates, + NameToCertificate: cfg.NameToCertificate, + GetCertificate: cfg.GetCertificate, + RootCAs: cfg.RootCAs, + NextProtos: cfg.NextProtos, + ServerName: cfg.ServerName, + ClientAuth: cfg.ClientAuth, + ClientCAs: cfg.ClientCAs, + InsecureSkipVerify: cfg.InsecureSkipVerify, + CipherSuites: cfg.CipherSuites, + PreferServerCipherSuites: cfg.PreferServerCipherSuites, + ClientSessionCache: cfg.ClientSessionCache, + MinVersion: cfg.MinVersion, + MaxVersion: cfg.MaxVersion, + CurvePreferences: cfg.CurvePreferences, + } +} diff --git a/vendor/github.com/gorilla/websocket/compression.go b/vendor/github.com/gorilla/websocket/compression.go new file mode 100644 index 0000000..813ffb1 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/compression.go @@ -0,0 +1,148 @@ +// Copyright 2017 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 ( + "compress/flate" + "errors" + "io" + "strings" + "sync" +) + +const ( + minCompressionLevel = -2 // flate.HuffmanOnly not defined in Go < 1.6 + maxCompressionLevel = flate.BestCompression + defaultCompressionLevel = 1 +) + +var ( + flateWriterPools [maxCompressionLevel - minCompressionLevel + 1]sync.Pool + flateReaderPool = sync.Pool{New: func() interface{} { + return flate.NewReader(nil) + }} +) + +func decompressNoContextTakeover(r io.Reader) io.ReadCloser { + const tail = + // Add four bytes as specified in RFC + "\x00\x00\xff\xff" + + // Add final block to squelch unexpected EOF error from flate reader. + "\x01\x00\x00\xff\xff" + + fr, _ := flateReaderPool.Get().(io.ReadCloser) + fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil) + return &flateReadWrapper{fr} +} + +func isValidCompressionLevel(level int) bool { + return minCompressionLevel <= level && level <= maxCompressionLevel +} + +func compressNoContextTakeover(w io.WriteCloser, level int) io.WriteCloser { + p := &flateWriterPools[level-minCompressionLevel] + tw := &truncWriter{w: w} + fw, _ := p.Get().(*flate.Writer) + if fw == nil { + fw, _ = flate.NewWriter(tw, level) + } else { + fw.Reset(tw) + } + return &flateWriteWrapper{fw: fw, tw: tw, p: p} +} + +// truncWriter is an io.Writer that writes all but the last four bytes of the +// stream to another io.Writer. +type truncWriter struct { + w io.WriteCloser + n int + p [4]byte +} + +func (w *truncWriter) Write(p []byte) (int, error) { + n := 0 + + // fill buffer first for simplicity. + if w.n < len(w.p) { + n = copy(w.p[w.n:], p) + p = p[n:] + w.n += n + if len(p) == 0 { + return n, nil + } + } + + m := len(p) + if m > len(w.p) { + m = len(w.p) + } + + if nn, err := w.w.Write(w.p[:m]); err != nil { + return n + nn, err + } + + copy(w.p[:], w.p[m:]) + copy(w.p[len(w.p)-m:], p[len(p)-m:]) + nn, err := w.w.Write(p[:len(p)-m]) + return n + nn, err +} + +type flateWriteWrapper struct { + fw *flate.Writer + tw *truncWriter + p *sync.Pool +} + +func (w *flateWriteWrapper) Write(p []byte) (int, error) { + if w.fw == nil { + return 0, errWriteClosed + } + return w.fw.Write(p) +} + +func (w *flateWriteWrapper) Close() error { + if w.fw == nil { + return errWriteClosed + } + err1 := w.fw.Flush() + w.p.Put(w.fw) + w.fw = nil + if w.tw.p != [4]byte{0, 0, 0xff, 0xff} { + return errors.New("websocket: internal error, unexpected bytes at end of flate stream") + } + err2 := w.tw.w.Close() + if err1 != nil { + return err1 + } + return err2 +} + +type flateReadWrapper struct { + fr io.ReadCloser +} + +func (r *flateReadWrapper) Read(p []byte) (int, error) { + if r.fr == nil { + return 0, io.ErrClosedPipe + } + n, err := r.fr.Read(p) + if err == io.EOF { + // Preemptively place the reader back in the pool. This helps with + // scenarios where the application does not call NextReader() soon after + // this final read. + r.Close() + } + return n, err +} + +func (r *flateReadWrapper) Close() error { + if r.fr == nil { + return io.ErrClosedPipe + } + err := r.fr.Close() + flateReaderPool.Put(r.fr) + r.fr = nil + return err +} diff --git a/vendor/github.com/gorilla/websocket/conn.go b/vendor/github.com/gorilla/websocket/conn.go new file mode 100644 index 0000000..97e1dba --- /dev/null +++ b/vendor/github.com/gorilla/websocket/conn.go @@ -0,0 +1,1149 @@ +// Copyright 2013 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 ( + "bufio" + "encoding/binary" + "errors" + "io" + "io/ioutil" + "math/rand" + "net" + "strconv" + "sync" + "time" + "unicode/utf8" +) + +const ( + // Frame header byte 0 bits from Section 5.2 of RFC 6455 + finalBit = 1 << 7 + rsv1Bit = 1 << 6 + rsv2Bit = 1 << 5 + rsv3Bit = 1 << 4 + + // Frame header byte 1 bits from Section 5.2 of RFC 6455 + maskBit = 1 << 7 + + maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask + maxControlFramePayloadSize = 125 + + writeWait = time.Second + + defaultReadBufferSize = 4096 + defaultWriteBufferSize = 4096 + + continuationFrame = 0 + noFrame = -1 +) + +// Close codes defined in RFC 6455, section 11.7. +const ( + CloseNormalClosure = 1000 + CloseGoingAway = 1001 + CloseProtocolError = 1002 + CloseUnsupportedData = 1003 + CloseNoStatusReceived = 1005 + CloseAbnormalClosure = 1006 + CloseInvalidFramePayloadData = 1007 + ClosePolicyViolation = 1008 + CloseMessageTooBig = 1009 + CloseMandatoryExtension = 1010 + CloseInternalServerErr = 1011 + CloseServiceRestart = 1012 + CloseTryAgainLater = 1013 + CloseTLSHandshake = 1015 +) + +// The message types are defined in RFC 6455, section 11.8. +const ( + // TextMessage denotes a text data message. The text message payload is + // interpreted as UTF-8 encoded text data. + TextMessage = 1 + + // BinaryMessage denotes a binary data message. + BinaryMessage = 2 + + // CloseMessage denotes a close control message. The optional message + // payload contains a numeric code and text. Use the FormatCloseMessage + // function to format a close message payload. + CloseMessage = 8 + + // PingMessage denotes a ping control message. The optional message payload + // is UTF-8 encoded text. + PingMessage = 9 + + // PongMessage denotes a ping control message. The optional message payload + // is UTF-8 encoded text. + PongMessage = 10 +) + +// ErrCloseSent is returned when the application writes a message to the +// connection after sending a close message. +var ErrCloseSent = errors.New("websocket: close sent") + +// ErrReadLimit is returned when reading a message that is larger than the +// read limit set for the connection. +var ErrReadLimit = errors.New("websocket: read limit exceeded") + +// netError satisfies the net Error interface. +type netError struct { + msg string + temporary bool + timeout bool +} + +func (e *netError) Error() string { return e.msg } +func (e *netError) Temporary() bool { return e.temporary } +func (e *netError) Timeout() bool { return e.timeout } + +// CloseError represents close frame. +type CloseError struct { + + // Code is defined in RFC 6455, section 11.7. + Code int + + // Text is the optional text payload. + Text string +} + +func (e *CloseError) Error() string { + s := []byte("websocket: close ") + s = strconv.AppendInt(s, int64(e.Code), 10) + switch e.Code { + case CloseNormalClosure: + s = append(s, " (normal)"...) + case CloseGoingAway: + s = append(s, " (going away)"...) + case CloseProtocolError: + s = append(s, " (protocol error)"...) + case CloseUnsupportedData: + s = append(s, " (unsupported data)"...) + case CloseNoStatusReceived: + s = append(s, " (no status)"...) + case CloseAbnormalClosure: + s = append(s, " (abnormal closure)"...) + case CloseInvalidFramePayloadData: + s = append(s, " (invalid payload data)"...) + case ClosePolicyViolation: + s = append(s, " (policy violation)"...) + case CloseMessageTooBig: + s = append(s, " (message too big)"...) + case CloseMandatoryExtension: + s = append(s, " (mandatory extension missing)"...) + case CloseInternalServerErr: + s = append(s, " (internal server error)"...) + case CloseTLSHandshake: + s = append(s, " (TLS handshake error)"...) + } + if e.Text != "" { + s = append(s, ": "...) + s = append(s, e.Text...) + } + return string(s) +} + +// IsCloseError returns boolean indicating whether the error is a *CloseError +// with one of the specified codes. +func IsCloseError(err error, codes ...int) bool { + if e, ok := err.(*CloseError); ok { + for _, code := range codes { + if e.Code == code { + return true + } + } + } + return false +} + +// IsUnexpectedCloseError returns boolean indicating whether the error is a +// *CloseError with a code not in the list of expected codes. +func IsUnexpectedCloseError(err error, expectedCodes ...int) bool { + if e, ok := err.(*CloseError); ok { + for _, code := range expectedCodes { + if e.Code == code { + return false + } + } + return true + } + return false +} + +var ( + errWriteTimeout = &netError{msg: "websocket: write timeout", timeout: true, temporary: true} + errUnexpectedEOF = &CloseError{Code: CloseAbnormalClosure, Text: io.ErrUnexpectedEOF.Error()} + errBadWriteOpCode = errors.New("websocket: bad write message type") + errWriteClosed = errors.New("websocket: write closed") + errInvalidControlFrame = errors.New("websocket: invalid control frame") +) + +func newMaskKey() [4]byte { + n := rand.Uint32() + return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)} +} + +func hideTempErr(err error) error { + if e, ok := err.(net.Error); ok && e.Temporary() { + err = &netError{msg: e.Error(), timeout: e.Timeout()} + } + return err +} + +func isControl(frameType int) bool { + return frameType == CloseMessage || frameType == PingMessage || frameType == PongMessage +} + +func isData(frameType int) bool { + return frameType == TextMessage || frameType == BinaryMessage +} + +var validReceivedCloseCodes = map[int]bool{ + // see http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number + + CloseNormalClosure: true, + CloseGoingAway: true, + CloseProtocolError: true, + CloseUnsupportedData: true, + CloseNoStatusReceived: false, + CloseAbnormalClosure: false, + CloseInvalidFramePayloadData: true, + ClosePolicyViolation: true, + CloseMessageTooBig: true, + CloseMandatoryExtension: true, + CloseInternalServerErr: true, + CloseServiceRestart: true, + CloseTryAgainLater: true, + CloseTLSHandshake: false, +} + +func isValidReceivedCloseCode(code int) bool { + return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999) +} + +// The Conn type represents a WebSocket connection. +type Conn struct { + conn net.Conn + isServer bool + subprotocol string + + // Write fields + mu chan bool // used as mutex to protect write to conn + writeBuf []byte // frame is constructed in this buffer. + writeDeadline time.Time + writer io.WriteCloser // the current writer returned to the application + isWriting bool // for best-effort concurrent write detection + + writeErrMu sync.Mutex + writeErr error + + enableWriteCompression bool + compressionLevel int + newCompressionWriter func(io.WriteCloser, int) io.WriteCloser + + // Read fields + reader io.ReadCloser // the current reader returned to the application + readErr error + br *bufio.Reader + readRemaining int64 // bytes remaining in current frame. + readFinal bool // true the current message has more frames. + readLength int64 // Message size. + readLimit int64 // Maximum message size. + readMaskPos int + readMaskKey [4]byte + handlePong func(string) error + handlePing func(string) error + handleClose func(int, string) error + readErrCount int + messageReader *messageReader // the current low-level reader + + readDecompress bool // whether last read frame had RSV1 set + newDecompressionReader func(io.Reader) io.ReadCloser +} + +func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn { + return newConnBRW(conn, isServer, readBufferSize, writeBufferSize, nil) +} + +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 { + 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) + } + + c := &Conn{ + isServer: isServer, + br: br, + conn: conn, + mu: mu, + readFinal: true, + writeBuf: writeBuf, + enableWriteCompression: true, + compressionLevel: defaultCompressionLevel, + } + c.SetCloseHandler(nil) + c.SetPingHandler(nil) + c.SetPongHandler(nil) + return c +} + +// Subprotocol returns the negotiated protocol for the connection. +func (c *Conn) Subprotocol() string { + return c.subprotocol +} + +// Close closes the underlying network connection without sending or waiting for a close frame. +func (c *Conn) Close() error { + return c.conn.Close() +} + +// LocalAddr returns the local network address. +func (c *Conn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +// RemoteAddr returns the remote network address. +func (c *Conn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +// Write methods + +func (c *Conn) writeFatal(err error) error { + err = hideTempErr(err) + c.writeErrMu.Lock() + if c.writeErr == nil { + c.writeErr = err + } + c.writeErrMu.Unlock() + return err +} + +func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error { + <-c.mu + defer func() { c.mu <- true }() + + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + if err != nil { + return err + } + + c.conn.SetWriteDeadline(deadline) + for _, buf := range bufs { + if len(buf) > 0 { + _, err := c.conn.Write(buf) + if err != nil { + return c.writeFatal(err) + } + } + } + + if frameType == CloseMessage { + c.writeFatal(ErrCloseSent) + } + return nil +} + +// WriteControl writes a control message with the given deadline. The allowed +// message types are CloseMessage, PingMessage and PongMessage. +func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) error { + if !isControl(messageType) { + return errBadWriteOpCode + } + if len(data) > maxControlFramePayloadSize { + return errInvalidControlFrame + } + + b0 := byte(messageType) | finalBit + b1 := byte(len(data)) + if !c.isServer { + b1 |= maskBit + } + + buf := make([]byte, 0, maxFrameHeaderSize+maxControlFramePayloadSize) + buf = append(buf, b0, b1) + + if c.isServer { + buf = append(buf, data...) + } else { + key := newMaskKey() + buf = append(buf, key[:]...) + buf = append(buf, data...) + maskBytes(key, 0, buf[6:]) + } + + d := time.Hour * 1000 + if !deadline.IsZero() { + d = deadline.Sub(time.Now()) + if d < 0 { + return errWriteTimeout + } + } + + timer := time.NewTimer(d) + select { + case <-c.mu: + timer.Stop() + case <-timer.C: + return errWriteTimeout + } + defer func() { c.mu <- true }() + + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + if err != nil { + return err + } + + c.conn.SetWriteDeadline(deadline) + _, err = c.conn.Write(buf) + if err != nil { + return c.writeFatal(err) + } + if messageType == CloseMessage { + c.writeFatal(ErrCloseSent) + } + return err +} + +func (c *Conn) prepWrite(messageType int) error { + // Close previous writer if not already closed by the application. It's + // probably better to return an error in this situation, but we cannot + // change this without breaking existing applications. + if c.writer != nil { + c.writer.Close() + c.writer = nil + } + + if !isControl(messageType) && !isData(messageType) { + return errBadWriteOpCode + } + + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + return err +} + +// NextWriter returns a writer for the next message to send. The writer's Close +// method flushes the complete message to the network. +// +// There can be at most one open writer on a connection. NextWriter closes the +// previous writer if the application has not already done so. +func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) { + if err := c.prepWrite(messageType); err != nil { + return nil, err + } + + mw := &messageWriter{ + c: c, + frameType: messageType, + pos: maxFrameHeaderSize, + } + c.writer = mw + if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) { + w := c.newCompressionWriter(c.writer, c.compressionLevel) + mw.compress = true + c.writer = w + } + return c.writer, nil +} + +type messageWriter struct { + c *Conn + compress bool // whether next call to flushFrame should set RSV1 + pos int // end of data in writeBuf. + frameType int // type of the current frame. + err error +} + +func (w *messageWriter) fatal(err error) error { + if w.err != nil { + w.err = err + w.c.writer = nil + } + return err +} + +// flushFrame writes buffered data and extra as a frame to the network. The +// final argument indicates that this is the last frame in the message. +func (w *messageWriter) flushFrame(final bool, extra []byte) error { + c := w.c + length := w.pos - maxFrameHeaderSize + len(extra) + + // Check for invalid control frames. + if isControl(w.frameType) && + (!final || length > maxControlFramePayloadSize) { + return w.fatal(errInvalidControlFrame) + } + + b0 := byte(w.frameType) + if final { + b0 |= finalBit + } + if w.compress { + b0 |= rsv1Bit + } + w.compress = false + + b1 := byte(0) + if !c.isServer { + b1 |= maskBit + } + + // Assume that the frame starts at beginning of c.writeBuf. + framePos := 0 + if c.isServer { + // Adjust up if mask not included in the header. + framePos = 4 + } + + switch { + case length >= 65536: + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | 127 + binary.BigEndian.PutUint64(c.writeBuf[framePos+2:], uint64(length)) + case length > 125: + framePos += 6 + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | 126 + binary.BigEndian.PutUint16(c.writeBuf[framePos+2:], uint16(length)) + default: + framePos += 8 + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | byte(length) + } + + if !c.isServer { + key := newMaskKey() + copy(c.writeBuf[maxFrameHeaderSize-4:], key[:]) + maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:w.pos]) + if len(extra) > 0 { + return c.writeFatal(errors.New("websocket: internal error, extra used in client mode")) + } + } + + // Write the buffers to the connection with best-effort detection of + // concurrent writes. See the concurrency section in the package + // documentation for more info. + + if c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = true + + err := c.write(w.frameType, c.writeDeadline, c.writeBuf[framePos:w.pos], extra) + + if !c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = false + + if err != nil { + return w.fatal(err) + } + + if final { + c.writer = nil + return nil + } + + // Setup for next frame. + w.pos = maxFrameHeaderSize + w.frameType = continuationFrame + return nil +} + +func (w *messageWriter) ncopy(max int) (int, error) { + n := len(w.c.writeBuf) - w.pos + if n <= 0 { + if err := w.flushFrame(false, nil); err != nil { + return 0, err + } + n = len(w.c.writeBuf) - w.pos + } + if n > max { + n = max + } + return n, nil +} + +func (w *messageWriter) Write(p []byte) (int, error) { + if w.err != nil { + return 0, w.err + } + + if len(p) > 2*len(w.c.writeBuf) && w.c.isServer { + // Don't buffer large messages. + err := w.flushFrame(false, p) + if err != nil { + return 0, err + } + return len(p), nil + } + + nn := len(p) + for len(p) > 0 { + n, err := w.ncopy(len(p)) + if err != nil { + return 0, err + } + copy(w.c.writeBuf[w.pos:], p[:n]) + w.pos += n + p = p[n:] + } + return nn, nil +} + +func (w *messageWriter) WriteString(p string) (int, error) { + if w.err != nil { + return 0, w.err + } + + nn := len(p) + for len(p) > 0 { + n, err := w.ncopy(len(p)) + if err != nil { + return 0, err + } + copy(w.c.writeBuf[w.pos:], p[:n]) + w.pos += n + p = p[n:] + } + return nn, nil +} + +func (w *messageWriter) ReadFrom(r io.Reader) (nn int64, err error) { + if w.err != nil { + return 0, w.err + } + for { + if w.pos == len(w.c.writeBuf) { + err = w.flushFrame(false, nil) + if err != nil { + break + } + } + var n int + n, err = r.Read(w.c.writeBuf[w.pos:]) + w.pos += n + nn += int64(n) + if err != nil { + if err == io.EOF { + err = nil + } + break + } + } + return nn, err +} + +func (w *messageWriter) Close() error { + if w.err != nil { + return w.err + } + if err := w.flushFrame(true, nil); err != nil { + return err + } + w.err = errWriteClosed + return nil +} + +// WritePreparedMessage writes prepared message into connection. +func (c *Conn) WritePreparedMessage(pm *PreparedMessage) error { + frameType, frameData, err := pm.frame(prepareKey{ + isServer: c.isServer, + compress: c.newCompressionWriter != nil && c.enableWriteCompression && isData(pm.messageType), + compressionLevel: c.compressionLevel, + }) + if err != nil { + return err + } + if c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = true + err = c.write(frameType, c.writeDeadline, frameData, nil) + if !c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = false + return err +} + +// WriteMessage is a helper method for getting a writer using NextWriter, +// writing the message and closing the writer. +func (c *Conn) WriteMessage(messageType int, data []byte) error { + + if c.isServer && (c.newCompressionWriter == nil || !c.enableWriteCompression) { + // Fast path with no allocations and single frame. + + if err := c.prepWrite(messageType); err != nil { + return err + } + mw := messageWriter{c: c, frameType: messageType, pos: maxFrameHeaderSize} + n := copy(c.writeBuf[mw.pos:], data) + mw.pos += n + data = data[n:] + return mw.flushFrame(true, data) + } + + w, err := c.NextWriter(messageType) + if err != nil { + return err + } + if _, err = w.Write(data); err != nil { + return err + } + return w.Close() +} + +// SetWriteDeadline sets the write deadline on the underlying network +// connection. After a write has timed out, the websocket state is corrupt and +// all future writes will return an error. A zero value for t means writes will +// not time out. +func (c *Conn) SetWriteDeadline(t time.Time) error { + c.writeDeadline = t + return nil +} + +// Read methods + +func (c *Conn) advanceFrame() (int, error) { + + // 1. Skip remainder of previous frame. + + if c.readRemaining > 0 { + if _, err := io.CopyN(ioutil.Discard, c.br, c.readRemaining); err != nil { + return noFrame, err + } + } + + // 2. Read and parse first two bytes of frame header. + + p, err := c.read(2) + if err != nil { + return noFrame, err + } + + final := p[0]&finalBit != 0 + frameType := int(p[0] & 0xf) + mask := p[1]&maskBit != 0 + c.readRemaining = int64(p[1] & 0x7f) + + c.readDecompress = false + if c.newDecompressionReader != nil && (p[0]&rsv1Bit) != 0 { + c.readDecompress = true + p[0] &^= rsv1Bit + } + + if rsv := p[0] & (rsv1Bit | rsv2Bit | rsv3Bit); rsv != 0 { + return noFrame, c.handleProtocolError("unexpected reserved bits 0x" + strconv.FormatInt(int64(rsv), 16)) + } + + switch frameType { + case CloseMessage, PingMessage, PongMessage: + if c.readRemaining > maxControlFramePayloadSize { + return noFrame, c.handleProtocolError("control frame length > 125") + } + if !final { + return noFrame, c.handleProtocolError("control frame not final") + } + case TextMessage, BinaryMessage: + if !c.readFinal { + return noFrame, c.handleProtocolError("message start before final message frame") + } + c.readFinal = final + case continuationFrame: + if c.readFinal { + return noFrame, c.handleProtocolError("continuation after final message frame") + } + c.readFinal = final + default: + return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType)) + } + + // 3. Read and parse frame length. + + switch c.readRemaining { + case 126: + p, err := c.read(2) + if err != nil { + return noFrame, err + } + c.readRemaining = int64(binary.BigEndian.Uint16(p)) + case 127: + p, err := c.read(8) + if err != nil { + return noFrame, err + } + c.readRemaining = int64(binary.BigEndian.Uint64(p)) + } + + // 4. Handle frame masking. + + if mask != c.isServer { + return noFrame, c.handleProtocolError("incorrect mask flag") + } + + if mask { + c.readMaskPos = 0 + p, err := c.read(len(c.readMaskKey)) + if err != nil { + return noFrame, err + } + copy(c.readMaskKey[:], p) + } + + // 5. For text and binary messages, enforce read limit and return. + + if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage { + + c.readLength += c.readRemaining + if c.readLimit > 0 && c.readLength > c.readLimit { + c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait)) + return noFrame, ErrReadLimit + } + + return frameType, nil + } + + // 6. Read control frame payload. + + var payload []byte + if c.readRemaining > 0 { + payload, err = c.read(int(c.readRemaining)) + c.readRemaining = 0 + if err != nil { + return noFrame, err + } + if c.isServer { + maskBytes(c.readMaskKey, 0, payload) + } + } + + // 7. Process control frame payload. + + switch frameType { + case PongMessage: + if err := c.handlePong(string(payload)); err != nil { + return noFrame, err + } + case PingMessage: + if err := c.handlePing(string(payload)); err != nil { + return noFrame, err + } + case CloseMessage: + closeCode := CloseNoStatusReceived + closeText := "" + if len(payload) >= 2 { + closeCode = int(binary.BigEndian.Uint16(payload)) + if !isValidReceivedCloseCode(closeCode) { + return noFrame, c.handleProtocolError("invalid close code") + } + closeText = string(payload[2:]) + if !utf8.ValidString(closeText) { + return noFrame, c.handleProtocolError("invalid utf8 payload in close frame") + } + } + if err := c.handleClose(closeCode, closeText); err != nil { + return noFrame, err + } + return noFrame, &CloseError{Code: closeCode, Text: closeText} + } + + return frameType, nil +} + +func (c *Conn) handleProtocolError(message string) error { + c.WriteControl(CloseMessage, FormatCloseMessage(CloseProtocolError, message), time.Now().Add(writeWait)) + return errors.New("websocket: " + message) +} + +// NextReader returns the next data message received from the peer. The +// returned messageType is either TextMessage or BinaryMessage. +// +// There can be at most one open reader on a connection. NextReader discards +// the previous message if the application has not already consumed it. +// +// Applications must break out of the application's read loop when this method +// returns a non-nil error value. Errors returned from this method are +// permanent. Once this method returns a non-nil error, all subsequent calls to +// this method return the same error. +func (c *Conn) NextReader() (messageType int, r io.Reader, err error) { + // Close previous reader, only relevant for decompression. + if c.reader != nil { + c.reader.Close() + c.reader = nil + } + + c.messageReader = nil + c.readLength = 0 + + for c.readErr == nil { + frameType, err := c.advanceFrame() + if err != nil { + c.readErr = hideTempErr(err) + break + } + if frameType == TextMessage || frameType == BinaryMessage { + c.messageReader = &messageReader{c} + c.reader = c.messageReader + if c.readDecompress { + c.reader = c.newDecompressionReader(c.reader) + } + return frameType, c.reader, nil + } + } + + // Applications that do handle the error returned from this method spin in + // tight loop on connection failure. To help application developers detect + // this error, panic on repeated reads to the failed connection. + c.readErrCount++ + if c.readErrCount >= 1000 { + panic("repeated read on failed websocket connection") + } + + return noFrame, nil, c.readErr +} + +type messageReader struct{ c *Conn } + +func (r *messageReader) Read(b []byte) (int, error) { + c := r.c + if c.messageReader != r { + return 0, io.EOF + } + + for c.readErr == nil { + + if c.readRemaining > 0 { + if int64(len(b)) > c.readRemaining { + b = b[:c.readRemaining] + } + n, err := c.br.Read(b) + c.readErr = hideTempErr(err) + if c.isServer { + c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n]) + } + c.readRemaining -= int64(n) + if c.readRemaining > 0 && c.readErr == io.EOF { + c.readErr = errUnexpectedEOF + } + return n, c.readErr + } + + if c.readFinal { + c.messageReader = nil + return 0, io.EOF + } + + frameType, err := c.advanceFrame() + switch { + case err != nil: + c.readErr = hideTempErr(err) + case frameType == TextMessage || frameType == BinaryMessage: + c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader") + } + } + + err := c.readErr + if err == io.EOF && c.messageReader == r { + err = errUnexpectedEOF + } + return 0, err +} + +func (r *messageReader) Close() error { + return nil +} + +// ReadMessage is a helper method for getting a reader using NextReader and +// reading from that reader to a buffer. +func (c *Conn) ReadMessage() (messageType int, p []byte, err error) { + var r io.Reader + messageType, r, err = c.NextReader() + if err != nil { + return messageType, nil, err + } + p, err = ioutil.ReadAll(r) + return messageType, p, err +} + +// SetReadDeadline sets the read deadline on the underlying network connection. +// After a read has timed out, the websocket connection state is corrupt and +// all future reads will return an error. A zero value for t means reads will +// not time out. +func (c *Conn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +// SetReadLimit sets the maximum size for a message read from the peer. If a +// message exceeds the limit, the connection sends a close frame to the peer +// and returns ErrReadLimit to the application. +func (c *Conn) SetReadLimit(limit int64) { + c.readLimit = limit +} + +// CloseHandler returns the current close handler +func (c *Conn) CloseHandler() func(code int, text string) error { + return c.handleClose +} + +// SetCloseHandler sets the handler for close messages received from the peer. +// The code argument to h is the received close code or CloseNoStatusReceived +// if the close message is empty. The default close handler sends a close frame +// back to the peer. +// +// The application must read the connection to process close messages as +// described in the section on Control Frames above. +// +// The connection read methods return a CloseError when a close frame is +// received. Most applications should handle close messages as part of their +// normal error handling. Applications should only set a close handler when the +// application must perform some action before sending a close frame back to +// the peer. +func (c *Conn) SetCloseHandler(h func(code int, text string) error) { + if h == nil { + h = func(code int, text string) error { + message := []byte{} + if code != CloseNoStatusReceived { + message = FormatCloseMessage(code, "") + } + c.WriteControl(CloseMessage, message, time.Now().Add(writeWait)) + return nil + } + } + c.handleClose = h +} + +// PingHandler returns the current ping handler +func (c *Conn) PingHandler() func(appData string) error { + return c.handlePing +} + +// SetPingHandler sets the handler for ping messages received from the peer. +// The appData argument to h is the PING frame application data. The default +// ping handler sends a pong to the peer. +// +// The application must read the connection to process ping messages as +// described in the section on Control Frames above. +func (c *Conn) SetPingHandler(h func(appData string) error) { + if h == nil { + h = func(message string) error { + err := c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait)) + if err == ErrCloseSent { + return nil + } else if e, ok := err.(net.Error); ok && e.Temporary() { + return nil + } + return err + } + } + c.handlePing = h +} + +// PongHandler returns the current pong handler +func (c *Conn) PongHandler() func(appData string) error { + return c.handlePong +} + +// SetPongHandler sets the handler for pong messages received from the peer. +// The appData argument to h is the PONG frame application data. The default +// pong handler does nothing. +// +// The application must read the connection to process ping messages as +// described in the section on Control Frames above. +func (c *Conn) SetPongHandler(h func(appData string) error) { + if h == nil { + h = func(string) error { return nil } + } + c.handlePong = h +} + +// UnderlyingConn returns the internal net.Conn. This can be used to further +// modifications to connection specific flags. +func (c *Conn) UnderlyingConn() net.Conn { + return c.conn +} + +// EnableWriteCompression enables and disables write compression of +// subsequent text and binary messages. This function is a noop if +// compression was not negotiated with the peer. +func (c *Conn) EnableWriteCompression(enable bool) { + c.enableWriteCompression = enable +} + +// SetCompressionLevel sets the flate compression level for subsequent text and +// binary messages. This function is a noop if compression was not negotiated +// with the peer. See the compress/flate package for a description of +// compression levels. +func (c *Conn) SetCompressionLevel(level int) error { + if !isValidCompressionLevel(level) { + return errors.New("websocket: invalid compression level") + } + c.compressionLevel = level + return nil +} + +// FormatCloseMessage formats closeCode and text as a WebSocket close message. +func FormatCloseMessage(closeCode int, text string) []byte { + buf := make([]byte, 2+len(text)) + binary.BigEndian.PutUint16(buf, uint16(closeCode)) + copy(buf[2:], text) + return buf +} diff --git a/vendor/github.com/gorilla/websocket/conn_read.go b/vendor/github.com/gorilla/websocket/conn_read.go new file mode 100644 index 0000000..1ea1505 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/conn_read.go @@ -0,0 +1,18 @@ +// 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 +} diff --git a/vendor/github.com/gorilla/websocket/conn_read_legacy.go b/vendor/github.com/gorilla/websocket/conn_read_legacy.go new file mode 100644 index 0000000..018541c --- /dev/null +++ b/vendor/github.com/gorilla/websocket/conn_read_legacy.go @@ -0,0 +1,21 @@ +// 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 +} diff --git a/vendor/github.com/gorilla/websocket/doc.go b/vendor/github.com/gorilla/websocket/doc.go new file mode 100644 index 0000000..e291a95 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/doc.go @@ -0,0 +1,180 @@ +// Copyright 2013 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 implements the WebSocket protocol defined in RFC 6455. +// +// Overview +// +// The Conn type represents a WebSocket connection. A server application uses +// the Upgrade function from an Upgrader object with a HTTP request handler +// to get a pointer to a Conn: +// +// var upgrader = websocket.Upgrader{ +// ReadBufferSize: 1024, +// WriteBufferSize: 1024, +// } +// +// func handler(w http.ResponseWriter, r *http.Request) { +// conn, err := upgrader.Upgrade(w, r, nil) +// if err != nil { +// log.Println(err) +// return +// } +// ... Use conn to send and receive messages. +// } +// +// Call the connection's WriteMessage and ReadMessage methods to send and +// receive messages as a slice of bytes. This snippet of code shows how to echo +// messages using these methods: +// +// for { +// messageType, p, err := conn.ReadMessage() +// if err != nil { +// return +// } +// if err = conn.WriteMessage(messageType, p); err != nil { +// return err +// } +// } +// +// In above snippet of code, p is a []byte and messageType is an int with value +// websocket.BinaryMessage or websocket.TextMessage. +// +// An application can also send and receive messages using the io.WriteCloser +// and io.Reader interfaces. To send a message, call the connection NextWriter +// method to get an io.WriteCloser, write the message to the writer and close +// the writer when done. To receive a message, call the connection NextReader +// method to get an io.Reader and read until io.EOF is returned. This snippet +// shows how to echo messages using the NextWriter and NextReader methods: +// +// for { +// messageType, r, err := conn.NextReader() +// if err != nil { +// return +// } +// w, err := conn.NextWriter(messageType) +// if err != nil { +// return err +// } +// if _, err := io.Copy(w, r); err != nil { +// return err +// } +// if err := w.Close(); err != nil { +// return err +// } +// } +// +// Data Messages +// +// The WebSocket protocol distinguishes between text and binary data messages. +// Text messages are interpreted as UTF-8 encoded text. The interpretation of +// binary messages is left to the application. +// +// This package uses the TextMessage and BinaryMessage integer constants to +// identify the two data message types. The ReadMessage and NextReader methods +// return the type of the received message. The messageType argument to the +// WriteMessage and NextWriter methods specifies the type of a sent message. +// +// It is the application's responsibility to ensure that text messages are +// valid UTF-8 encoded text. +// +// Control Messages +// +// The WebSocket protocol defines three types of control messages: close, ping +// and pong. Call the connection WriteControl, WriteMessage or NextWriter +// methods to send a control message to the peer. +// +// Connections handle received close messages by sending a close message to the +// peer and returning a *CloseError from the the NextReader, ReadMessage or the +// message Read method. +// +// Connections handle received ping and pong messages by invoking callback +// functions set with SetPingHandler and SetPongHandler methods. The callback +// functions are called from the NextReader, ReadMessage and the message Read +// methods. +// +// The default ping handler sends a pong to the peer. The application's reading +// goroutine can block for a short time while the handler writes the pong data +// to the connection. +// +// The application must read the connection to process ping, pong and close +// messages sent from the peer. If the application is not otherwise interested +// in messages from the peer, then the application should start a goroutine to +// read and discard messages from the peer. A simple example is: +// +// func readLoop(c *websocket.Conn) { +// for { +// if _, _, err := c.NextReader(); err != nil { +// c.Close() +// break +// } +// } +// } +// +// Concurrency +// +// Connections support one concurrent reader and one concurrent writer. +// +// Applications are responsible for ensuring that no more than one goroutine +// calls the write methods (NextWriter, SetWriteDeadline, WriteMessage, +// WriteJSON, EnableWriteCompression, SetCompressionLevel) concurrently and +// that no more than one goroutine calls the read methods (NextReader, +// SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, SetPingHandler) +// concurrently. +// +// The Close and WriteControl methods can be called concurrently with all other +// methods. +// +// Origin Considerations +// +// Web browsers allow Javascript applications to open a WebSocket connection to +// any host. It's up to the server to enforce an origin policy using the Origin +// request header sent by the browser. +// +// The Upgrader calls the function specified in the CheckOrigin field to check +// the origin. If the CheckOrigin function returns false, then the Upgrade +// method fails the WebSocket handshake with HTTP status 403. +// +// If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail +// the handshake if the Origin request header is present and not equal to the +// Host request header. +// +// An application can allow connections from any origin by specifying a +// function that always returns true: +// +// var upgrader = websocket.Upgrader{ +// CheckOrigin: func(r *http.Request) bool { return true }, +// } +// +// The deprecated Upgrade function does not enforce an origin policy. It's the +// application's responsibility to check the Origin header before calling +// Upgrade. +// +// Compression EXPERIMENTAL +// +// Per message compression extensions (RFC 7692) are experimentally supported +// by this package in a limited capacity. Setting the EnableCompression option +// to true in Dialer or Upgrader will attempt to negotiate per message deflate +// support. +// +// var upgrader = websocket.Upgrader{ +// EnableCompression: true, +// } +// +// If compression was successfully negotiated with the connection's peer, any +// message received in compressed form will be automatically decompressed. +// All Read methods will return uncompressed bytes. +// +// Per message compression of messages written to a connection can be enabled +// or disabled by calling the corresponding Conn method: +// +// conn.EnableWriteCompression(false) +// +// Currently this package does not support compression with "context takeover". +// This means that messages must be compressed and decompressed in isolation, +// without retaining sliding window or dictionary state across messages. For +// more details refer to RFC 7692. +// +// Use of compression is experimental and may result in decreased performance. +package websocket diff --git a/vendor/github.com/gorilla/websocket/json.go b/vendor/github.com/gorilla/websocket/json.go new file mode 100644 index 0000000..4f0e368 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/json.go @@ -0,0 +1,55 @@ +// Copyright 2013 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 ( + "encoding/json" + "io" +) + +// WriteJSON is deprecated, use c.WriteJSON instead. +func WriteJSON(c *Conn, v interface{}) error { + return c.WriteJSON(v) +} + +// WriteJSON writes the JSON encoding of v to the connection. +// +// See the documentation for encoding/json Marshal for details about the +// conversion of Go values to JSON. +func (c *Conn) WriteJSON(v interface{}) error { + w, err := c.NextWriter(TextMessage) + if err != nil { + return err + } + err1 := json.NewEncoder(w).Encode(v) + err2 := w.Close() + if err1 != nil { + return err1 + } + return err2 +} + +// ReadJSON is deprecated, use c.ReadJSON instead. +func ReadJSON(c *Conn, v interface{}) error { + return c.ReadJSON(v) +} + +// ReadJSON reads the next JSON-encoded message from the connection and stores +// it in the value pointed to by v. +// +// See the documentation for the encoding/json Unmarshal function for details +// about the conversion of JSON to a Go value. +func (c *Conn) ReadJSON(v interface{}) error { + _, r, err := c.NextReader() + if err != nil { + return err + } + err = json.NewDecoder(r).Decode(v) + if err == io.EOF { + // One value is expected in the message. + err = io.ErrUnexpectedEOF + } + return err +} diff --git a/vendor/github.com/gorilla/websocket/mask.go b/vendor/github.com/gorilla/websocket/mask.go new file mode 100644 index 0000000..6a88bbc --- /dev/null +++ b/vendor/github.com/gorilla/websocket/mask.go @@ -0,0 +1,55 @@ +// 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 !appengine + +package websocket + +import "unsafe" + +const wordSize = int(unsafe.Sizeof(uintptr(0))) + +func maskBytes(key [4]byte, pos int, b []byte) int { + + // Mask one byte at a time for small buffers. + if len(b) < 2*wordSize { + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + return pos & 3 + } + + // Mask one byte at a time to word boundary. + if n := int(uintptr(unsafe.Pointer(&b[0]))) % wordSize; n != 0 { + n = wordSize - n + for i := range b[:n] { + b[i] ^= key[pos&3] + pos++ + } + b = b[n:] + } + + // Create aligned word size key. + var k [wordSize]byte + for i := range k { + k[i] = key[(pos+i)&3] + } + kw := *(*uintptr)(unsafe.Pointer(&k)) + + // Mask one word at a time. + n := (len(b) / wordSize) * wordSize + for i := 0; i < n; i += wordSize { + *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw + } + + // Mask one byte at a time for remaining bytes. + b = b[n:] + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + + return pos & 3 +} diff --git a/vendor/github.com/gorilla/websocket/mask_safe.go b/vendor/github.com/gorilla/websocket/mask_safe.go new file mode 100644 index 0000000..2aac060 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/mask_safe.go @@ -0,0 +1,15 @@ +// 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 appengine + +package websocket + +func maskBytes(key [4]byte, pos int, b []byte) int { + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + return pos & 3 +} diff --git a/vendor/github.com/gorilla/websocket/prepared.go b/vendor/github.com/gorilla/websocket/prepared.go new file mode 100644 index 0000000..1efffbd --- /dev/null +++ b/vendor/github.com/gorilla/websocket/prepared.go @@ -0,0 +1,103 @@ +// Copyright 2017 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 ( + "bytes" + "net" + "sync" + "time" +) + +// PreparedMessage caches on the wire representations of a message payload. +// Use PreparedMessage to efficiently send a message payload to multiple +// connections. PreparedMessage is especially useful when compression is used +// because the CPU and memory expensive compression operation can be executed +// once for a given set of compression options. +type PreparedMessage struct { + messageType int + data []byte + err error + mu sync.Mutex + frames map[prepareKey]*preparedFrame +} + +// prepareKey defines a unique set of options to cache prepared frames in PreparedMessage. +type prepareKey struct { + isServer bool + compress bool + compressionLevel int +} + +// preparedFrame contains data in wire representation. +type preparedFrame struct { + once sync.Once + data []byte +} + +// NewPreparedMessage returns an initialized PreparedMessage. You can then send +// it to connection using WritePreparedMessage method. Valid wire +// representation will be calculated lazily only once for a set of current +// connection options. +func NewPreparedMessage(messageType int, data []byte) (*PreparedMessage, error) { + pm := &PreparedMessage{ + messageType: messageType, + frames: make(map[prepareKey]*preparedFrame), + data: data, + } + + // Prepare a plain server frame. + _, frameData, err := pm.frame(prepareKey{isServer: true, compress: false}) + if err != nil { + return nil, err + } + + // To protect against caller modifying the data argument, remember the data + // copied to the plain server frame. + pm.data = frameData[len(frameData)-len(data):] + return pm, nil +} + +func (pm *PreparedMessage) frame(key prepareKey) (int, []byte, error) { + pm.mu.Lock() + frame, ok := pm.frames[key] + if !ok { + frame = &preparedFrame{} + pm.frames[key] = frame + } + pm.mu.Unlock() + + var err error + frame.once.Do(func() { + // Prepare a frame using a 'fake' connection. + // TODO: Refactor code in conn.go to allow more direct construction of + // the frame. + mu := make(chan bool, 1) + mu <- true + var nc prepareConn + c := &Conn{ + conn: &nc, + mu: mu, + isServer: key.isServer, + compressionLevel: key.compressionLevel, + enableWriteCompression: true, + writeBuf: make([]byte, defaultWriteBufferSize+maxFrameHeaderSize), + } + if key.compress { + c.newCompressionWriter = compressNoContextTakeover + } + err = c.WriteMessage(pm.messageType, pm.data) + frame.data = nc.buf.Bytes() + }) + return pm.messageType, frame.data, err +} + +type prepareConn struct { + buf bytes.Buffer + net.Conn +} + +func (pc *prepareConn) Write(p []byte) (int, error) { return pc.buf.Write(p) } +func (pc *prepareConn) SetWriteDeadline(t time.Time) error { return nil } diff --git a/vendor/github.com/gorilla/websocket/server.go b/vendor/github.com/gorilla/websocket/server.go new file mode 100644 index 0000000..3495e0f --- /dev/null +++ b/vendor/github.com/gorilla/websocket/server.go @@ -0,0 +1,291 @@ +// Copyright 2013 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 ( + "bufio" + "errors" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +// HandshakeError describes an error with the handshake from the peer. +type HandshakeError struct { + message string +} + +func (e HandshakeError) Error() string { return e.message } + +// Upgrader specifies parameters for upgrading an HTTP connection to a +// WebSocket connection. +type Upgrader struct { + // HandshakeTimeout specifies the duration for the handshake to complete. + HandshakeTimeout time.Duration + + // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer + // 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 + // or received. + ReadBufferSize, WriteBufferSize int + + // Subprotocols specifies the server's supported protocols in order of + // preference. If this field is set, then the Upgrade method negotiates a + // subprotocol by selecting the first match in this list with a protocol + // requested by the client. + Subprotocols []string + + // Error specifies the function for generating HTTP error responses. If Error + // is nil, then http.Error is used to generate the HTTP response. + Error func(w http.ResponseWriter, r *http.Request, status int, reason error) + + // CheckOrigin returns true if the request Origin header is acceptable. If + // CheckOrigin is nil, the host in the Origin header must not be set or + // must match the host of the request. + CheckOrigin func(r *http.Request) bool + + // EnableCompression specify if the server should attempt to negotiate per + // message compression (RFC 7692). Setting this value to true does not + // guarantee that compression will be supported. Currently only "no context + // takeover" modes are supported. + EnableCompression bool +} + +func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) { + err := HandshakeError{reason} + if u.Error != nil { + u.Error(w, r, status, err) + } else { + w.Header().Set("Sec-Websocket-Version", "13") + http.Error(w, http.StatusText(status), status) + } + return nil, err +} + +// checkSameOrigin returns true if the origin is not set or is equal to the request host. +func checkSameOrigin(r *http.Request) bool { + origin := r.Header["Origin"] + if len(origin) == 0 { + return true + } + u, err := url.Parse(origin[0]) + if err != nil { + return false + } + return u.Host == r.Host +} + +func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string { + if u.Subprotocols != nil { + clientProtocols := Subprotocols(r) + for _, serverProtocol := range u.Subprotocols { + for _, clientProtocol := range clientProtocols { + if clientProtocol == serverProtocol { + return clientProtocol + } + } + } + } else if responseHeader != nil { + return responseHeader.Get("Sec-Websocket-Protocol") + } + return "" +} + +// Upgrade upgrades the HTTP server connection to the WebSocket protocol. +// +// The responseHeader is included in the response to the client's upgrade +// request. Use the responseHeader to specify cookies (Set-Cookie) and the +// application negotiated subprotocol (Sec-Websocket-Protocol). +// +// If the upgrade fails, then Upgrade replies to the client with an HTTP error +// response. +func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) { + if r.Method != "GET" { + return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: not a websocket handshake: request method is not GET") + } + + if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok { + return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-Websocket-Extensions' headers are unsupported") + } + + if !tokenListContainsValue(r.Header, "Connection", "upgrade") { + return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'upgrade' token not found in 'Connection' header") + } + + if !tokenListContainsValue(r.Header, "Upgrade", "websocket") { + return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'websocket' token not found in 'Upgrade' header") + } + + if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") { + return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header") + } + + checkOrigin := u.CheckOrigin + if checkOrigin == nil { + checkOrigin = checkSameOrigin + } + if !checkOrigin(r) { + return u.returnError(w, r, http.StatusForbidden, "websocket: 'Origin' header value not allowed") + } + + challengeKey := r.Header.Get("Sec-Websocket-Key") + if challengeKey == "" { + 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) + + // Negotiate PMCE + var compress bool + if u.EnableCompression { + for _, ext := range parseExtensions(r.Header) { + if ext[""] != "permessage-deflate" { + continue + } + compress = true + break + } + } + + 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() + if err != nil { + return u.returnError(w, r, http.StatusInternalServerError, err.Error()) + } + + if brw.Reader.Buffered() > 0 { + netConn.Close() + return nil, errors.New("websocket: client sent data before handshake is complete") + } + + c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw) + c.subprotocol = subprotocol + + if compress { + c.newCompressionWriter = compressNoContextTakeover + c.newDecompressionReader = decompressNoContextTakeover + } + + p := c.writeBuf[: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"...) + if c.subprotocol != "" { + p = append(p, "Sec-Websocket-Protocol: "...) + p = append(p, c.subprotocol...) + p = append(p, "\r\n"...) + } + if compress { + p = append(p, "Sec-Websocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...) + } + for k, vs := range responseHeader { + if k == "Sec-Websocket-Protocol" { + continue + } + for _, v := range vs { + p = append(p, k...) + p = append(p, ": "...) + for i := 0; i < len(v); i++ { + b := v[i] + if b <= 31 { + // prevent response splitting. + b = ' ' + } + p = append(p, b) + } + p = append(p, "\r\n"...) + } + } + p = append(p, "\r\n"...) + + // Clear deadlines set by HTTP server. + netConn.SetDeadline(time.Time{}) + + if u.HandshakeTimeout > 0 { + netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout)) + } + if _, err = netConn.Write(p); err != nil { + netConn.Close() + return nil, err + } + if u.HandshakeTimeout > 0 { + netConn.SetWriteDeadline(time.Time{}) + } + + return c, nil +} + +// Upgrade upgrades the HTTP server connection to the WebSocket protocol. +// +// This function is deprecated, use websocket.Upgrader instead. +// +// The application is responsible for checking the request origin before +// calling Upgrade. An example implementation of the same origin policy is: +// +// if req.Header.Get("Origin") != "http://"+req.Host { +// http.Error(w, "Origin not allowed", 403) +// return +// } +// +// If the endpoint supports subprotocols, then the application is responsible +// for negotiating the protocol used on the connection. Use the Subprotocols() +// function to get the subprotocols requested by the client. Use the +// Sec-Websocket-Protocol response header to specify the subprotocol selected +// by the application. +// +// The responseHeader is included in the response to the client's upgrade +// request. Use the responseHeader to specify cookies (Set-Cookie) and the +// negotiated subprotocol (Sec-Websocket-Protocol). +// +// The connection buffers IO to the underlying network connection. The +// readBufSize and writeBufSize parameters specify the size of the buffers to +// use. Messages can be larger than the buffers. +// +// If the request is not a valid WebSocket handshake, then Upgrade returns an +// error of type HandshakeError. Applications should handle this error by +// replying to the client with an HTTP error response. +func Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header, readBufSize, writeBufSize int) (*Conn, error) { + u := Upgrader{ReadBufferSize: readBufSize, WriteBufferSize: writeBufSize} + u.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) { + // don't return errors to maintain backwards compatibility + } + u.CheckOrigin = func(r *http.Request) bool { + // allow all connections by default + return true + } + return u.Upgrade(w, r, responseHeader) +} + +// Subprotocols returns the subprotocols requested by the client in the +// Sec-Websocket-Protocol header. +func Subprotocols(r *http.Request) []string { + h := strings.TrimSpace(r.Header.Get("Sec-Websocket-Protocol")) + if h == "" { + return nil + } + protocols := strings.Split(h, ",") + for i := range protocols { + protocols[i] = strings.TrimSpace(protocols[i]) + } + return protocols +} + +// IsWebSocketUpgrade returns true if the client requested upgrade to the +// WebSocket protocol. +func IsWebSocketUpgrade(r *http.Request) bool { + return tokenListContainsValue(r.Header, "Connection", "upgrade") && + tokenListContainsValue(r.Header, "Upgrade", "websocket") +} diff --git a/vendor/github.com/gorilla/websocket/util.go b/vendor/github.com/gorilla/websocket/util.go new file mode 100644 index 0000000..9a4908d --- /dev/null +++ b/vendor/github.com/gorilla/websocket/util.go @@ -0,0 +1,214 @@ +// Copyright 2013 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 ( + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "io" + "net/http" + "strings" +) + +var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + +func computeAcceptKey(challengeKey string) string { + h := sha1.New() + h.Write([]byte(challengeKey)) + h.Write(keyGUID) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +func generateChallengeKey() (string, error) { + p := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, p); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(p), nil +} + +// Octet types from RFC 2616. +var octetTypes [256]byte + +const ( + isTokenOctet = 1 << iota + isSpaceOctet +) + +func init() { + // From RFC 2616 + // + // OCTET = + // CHAR = + // CTL = + // CR = + // LF = + // SP = + // HT = + // <"> = + // CRLF = CR LF + // LWS = [CRLF] 1*( SP | HT ) + // TEXT = + // separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> + // | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT + // token = 1* + // qdtext = > + + for c := 0; c < 256; c++ { + var t byte + isCtl := c <= 31 || c == 127 + isChar := 0 <= c && c <= 127 + isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0 + if strings.IndexRune(" \t\r\n", rune(c)) >= 0 { + t |= isSpaceOctet + } + if isChar && !isCtl && !isSeparator { + t |= isTokenOctet + } + octetTypes[c] = t + } +} + +func skipSpace(s string) (rest string) { + i := 0 + for ; i < len(s); i++ { + if octetTypes[s[i]]&isSpaceOctet == 0 { + break + } + } + return s[i:] +} + +func nextToken(s string) (token, rest string) { + i := 0 + for ; i < len(s); i++ { + if octetTypes[s[i]]&isTokenOctet == 0 { + break + } + } + return s[:i], s[i:] +} + +func nextTokenOrQuoted(s string) (value string, rest string) { + if !strings.HasPrefix(s, "\"") { + return nextToken(s) + } + s = s[1:] + for i := 0; i < len(s); i++ { + switch s[i] { + case '"': + return s[:i], s[i+1:] + case '\\': + p := make([]byte, len(s)-1) + j := copy(p, s[:i]) + escape := true + for i = i + 1; i < len(s); i++ { + b := s[i] + switch { + case escape: + escape = false + p[j] = b + j += 1 + case b == '\\': + escape = true + case b == '"': + return string(p[:j]), s[i+1:] + default: + p[j] = b + j += 1 + } + } + return "", "" + } + } + return "", "" +} + +// tokenListContainsValue returns true if the 1#token header with the given +// name contains token. +func tokenListContainsValue(header http.Header, name string, value string) bool { +headers: + for _, s := range header[name] { + for { + var t string + t, s = nextToken(skipSpace(s)) + if t == "" { + continue headers + } + s = skipSpace(s) + if s != "" && s[0] != ',' { + continue headers + } + if strings.EqualFold(t, value) { + return true + } + if s == "" { + continue headers + } + s = s[1:] + } + } + return false +} + +// parseExtensiosn parses WebSocket extensions from a header. +func parseExtensions(header http.Header) []map[string]string { + + // From RFC 6455: + // + // Sec-WebSocket-Extensions = extension-list + // extension-list = 1#extension + // extension = extension-token *( ";" extension-param ) + // extension-token = registered-token + // registered-token = token + // extension-param = token [ "=" (token | quoted-string) ] + // ;When using the quoted-string syntax variant, the value + // ;after quoted-string unescaping MUST conform to the + // ;'token' ABNF. + + var result []map[string]string +headers: + for _, s := range header["Sec-Websocket-Extensions"] { + for { + var t string + t, s = nextToken(skipSpace(s)) + if t == "" { + continue headers + } + ext := map[string]string{"": t} + for { + s = skipSpace(s) + if !strings.HasPrefix(s, ";") { + break + } + var k string + k, s = nextToken(skipSpace(s[1:])) + if k == "" { + continue headers + } + s = skipSpace(s) + var v string + if strings.HasPrefix(s, "=") { + v, s = nextTokenOrQuoted(skipSpace(s[1:])) + s = skipSpace(s) + } + if s != "" && s[0] != ',' && s[0] != ';' { + continue headers + } + ext[k] = v + } + if s != "" && s[0] != ',' { + continue headers + } + result = append(result, ext) + if s == "" { + continue headers + } + s = s[1:] + } + } + return result +} diff --git a/vendor/github.com/maruel/panicparse/stack/source.go b/vendor/github.com/maruel/panicparse/stack/source.go index a747250..f09e673 100644 --- a/vendor/github.com/maruel/panicparse/stack/source.go +++ b/vendor/github.com/maruel/panicparse/stack/source.go @@ -53,7 +53,7 @@ func (c *cache) augmentGoroutine(goroutine *Goroutine) { } // Once all loaded, we can look at the next call when available. - for i := 0; i < len(goroutine.Stack.Calls)-1; i++ { + for i := 1; i < len(goroutine.Stack.Calls); i++ { // Get the AST from the previous call and process the call line with it. if f := c.getFuncAST(&goroutine.Stack.Calls[i]); f != nil { processCall(&goroutine.Stack.Calls[i], f) @@ -115,15 +115,6 @@ type parsedFile struct { // getFuncAST gets the callee site function AST representation for the code // inside the function f at line l. func (p *parsedFile) getFuncAST(f string, l int) (d *ast.FuncDecl) { - if len(p.lineToByteOffset) <= l { - // The line number in the stack trace line does not exist in the file. That - // can only mean that the sources on disk do not match the sources used to - // build the binary. - // TODO(maruel): This should be surfaced, so that source parsing is - // completely ignored. - return - } - // Walk the AST to find the lineToByteOffset that fits the line number. var lastFunc *ast.FuncDecl var found ast.Node @@ -164,18 +155,20 @@ func (p *parsedFile) getFuncAST(f string, l int) (d *ast.FuncDecl) { } func name(n ast.Node) string { - switch t := n.(type) { - case *ast.InterfaceType: + if _, ok := n.(*ast.InterfaceType); ok { return "interface{}" - case *ast.Ident: - return t.Name - case *ast.SelectorExpr: - return t.Sel.Name - case *ast.StarExpr: - return "*" + name(t.X) - default: - return "" } + if i, ok := n.(*ast.Ident); ok { + return i.Name + } + if _, ok := n.(*ast.FuncType); ok { + return "func" + } + if s, ok := n.(*ast.SelectorExpr); ok { + return s.Sel.Name + } + // TODO(maruel): Implement anything missing. + return "" } // fieldToType returns the type name and whether if it's an ellipsis. @@ -196,10 +189,6 @@ func fieldToType(f *ast.Field) (string, bool) { return arg.Sel.Name, false case *ast.StarExpr: return "*" + name(arg.X), false - case *ast.MapType: - return fmt.Sprintf("map[%s]%s", name(arg.Key), name(arg.Value)), false - case *ast.ChanType: - return fmt.Sprintf("chan %s", name(arg.Value)), false default: // TODO(maruel): Implement anything missing. return "", false diff --git a/vendor/github.com/maruel/panicparse/stack/stack.go b/vendor/github.com/maruel/panicparse/stack/stack.go index a9917ed..cfb502e 100644 --- a/vendor/github.com/maruel/panicparse/stack/stack.go +++ b/vendor/github.com/maruel/panicparse/stack/stack.go @@ -35,7 +35,7 @@ var ( // - found next stack barrier at 0x123; expected // - runtime: unexpected return pc for FUNC_NAME called from 0x123 - reRoutineHeader = regexp.MustCompile("^goroutine (\\d+) \\[([^\\]]+)\\]\\:\r?\n$") + reRoutineHeader = regexp.MustCompile("^goroutine (\\d+) \\[([^\\]]+)\\]\\:\n$") reMinutes = regexp.MustCompile("^(\\d+) minutes$") reUnavail = regexp.MustCompile("^(?:\t| +)goroutine running on other thread; stack unavailable") // See gentraceback() in src/runtime/traceback.go for more information. @@ -54,12 +54,12 @@ var ( // when a signal is not correctly handled. It is printed with m.throwing>0. // These are discarded. // - For cgo, the source file may be "??". - reFile = regexp.MustCompile("^(?:\t| +)(\\?\\?|\\|.+\\.(?:c|go|s))\\:(\\d+)(?:| \\+0x[0-9a-f]+)(?:| fp=0x[0-9a-f]+ sp=0x[0-9a-f]+)\r?\n$") + reFile = regexp.MustCompile("^(?:\t| +)(\\?\\?|\\|.+\\.(?:c|go|s))\\:(\\d+)(?:| \\+0x[0-9a-f]+)(?:| fp=0x[0-9a-f]+ sp=0x[0-9a-f]+)\n$") // Sadly, it doesn't note the goroutine number so we could cascade them per // parenthood. - reCreated = regexp.MustCompile("^created by (.+)\r?\n$") - reFunc = regexp.MustCompile("^(.+)\\((.*)\\)\r?\n$") - reElided = regexp.MustCompile("^\\.\\.\\.additional frames elided\\.\\.\\.\r?\n$") + reCreated = regexp.MustCompile("^created by (.+)\n$") + reFunc = regexp.MustCompile("^(.+)\\((.*)\\)\n$") + reElided = regexp.MustCompile("^\\.\\.\\.additional frames elided\\.\\.\\.\n$") // Include frequent GOROOT value on Windows, distro provided and user // installed path. This simplifies the user's life when processing a trace // generated on another VM. @@ -656,7 +656,7 @@ func ParseDump(r io.Reader, out io.Writer) ([]Goroutine, error) { firstLine := false for scanner.Scan() { line := scanner.Text() - if line == "\n" || line == "\r\n" { + if line == "\n" { if goroutine != nil { goroutine = nil continue diff --git a/vendor/github.com/mattn/go-runewidth/.travis.yml b/vendor/github.com/mattn/go-runewidth/.travis.yml new file mode 100644 index 0000000..5c9c2a3 --- /dev/null +++ b/vendor/github.com/mattn/go-runewidth/.travis.yml @@ -0,0 +1,8 @@ +language: go +go: + - tip +before_install: + - go get github.com/mattn/goveralls + - go get golang.org/x/tools/cmd/cover +script: + - $HOME/gopath/bin/goveralls -repotoken lAKAWPzcGsD3A8yBX3BGGtRUdJ6CaGERL diff --git a/vendor/github.com/mattn/go-runewidth/runewidth.go b/vendor/github.com/mattn/go-runewidth/runewidth.go index a16f1af..2164497 100644 --- a/vendor/github.com/mattn/go-runewidth/runewidth.go +++ b/vendor/github.com/mattn/go-runewidth/runewidth.go @@ -55,7 +55,6 @@ var private = table{ var nonprint = table{ {0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD}, {0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F}, - {0x2028, 0x2029}, {0x202A, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF}, {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF}, } diff --git a/vendor/github.com/nlopes/slack/.gitignore b/vendor/github.com/nlopes/slack/.gitignore new file mode 100644 index 0000000..dd2440d --- /dev/null +++ b/vendor/github.com/nlopes/slack/.gitignore @@ -0,0 +1,2 @@ +*.test +*~ diff --git a/vendor/github.com/nlopes/slack/.travis.yml b/vendor/github.com/nlopes/slack/.travis.yml new file mode 100644 index 0000000..bd0539e --- /dev/null +++ b/vendor/github.com/nlopes/slack/.travis.yml @@ -0,0 +1,21 @@ +language: go + +go: + - 1.7.x + - 1.8.x + - 1.9.x + - tip + +before_install: + - export PATH=$HOME/gopath/bin:$PATH + +script: + - go test -race ./... + - go test -cover ./... + +matrix: + allow_failures: + - go: tip + +git: + depth: 10 diff --git a/vendor/github.com/nlopes/slack/README.md b/vendor/github.com/nlopes/slack/README.md index 4d3abdb..953b9d8 100644 --- a/vendor/github.com/nlopes/slack/README.md +++ b/vendor/github.com/nlopes/slack/README.md @@ -1,6 +1,8 @@ Slack API in Go [![GoDoc](https://godoc.org/github.com/nlopes/slack?status.svg)](https://godoc.org/github.com/nlopes/slack) [![Build Status](https://travis-ci.org/nlopes/slack.svg)](https://travis-ci.org/nlopes/slack) =============== +[![Join the chat at https://gitter.im/go-slack/Lobby](https://badges.gitter.im/go-slack/Lobby.svg)](https://gitter.im/go-slack/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + This library supports most if not all of the `api.slack.com` REST calls, as well as the Real-Time Messaging protocol over websocket, in a fully managed way. diff --git a/vendor/github.com/nlopes/slack/admin.go b/vendor/github.com/nlopes/slack/admin.go index 478c4f4..4a7e0b1 100644 --- a/vendor/github.com/nlopes/slack/admin.go +++ b/vendor/github.com/nlopes/slack/admin.go @@ -12,9 +12,9 @@ type adminResponse struct { Error string `json:"error"` } -func adminRequest(ctx context.Context, method string, teamName string, values url.Values, debug bool) (*adminResponse, error) { +func adminRequest(ctx context.Context, client HTTPRequester, method string, teamName string, values url.Values, debug bool) (*adminResponse, error) { adminResponse := &adminResponse{} - err := parseAdminResponse(ctx, method, teamName, values, adminResponse, debug) + err := parseAdminResponse(ctx, client, method, teamName, values, adminResponse, debug) if err != nil { return nil, err } @@ -35,12 +35,12 @@ func (api *Client) DisableUser(teamName string, uid string) error { func (api *Client) DisableUserContext(ctx context.Context, teamName string, uid string) error { values := url.Values{ "user": {uid}, - "token": {api.config.token}, + "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "setInactive", teamName, values, api.debug) + _, 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) } @@ -61,12 +61,12 @@ func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, fi "first_name": {firstName}, "last_name": {lastName}, "ultra_restricted": {"1"}, - "token": {api.config.token}, + "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "invite", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to invite single-channel guest: %s", err) } @@ -87,12 +87,12 @@ func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channe "first_name": {firstName}, "last_name": {lastName}, "restricted": {"1"}, - "token": {api.config.token}, + "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "invite", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to restricted account: %s", err) } @@ -111,12 +111,12 @@ func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName, "email": {emailAddress}, "first_name": {firstName}, "last_name": {lastName}, - "token": {api.config.token}, + "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "invite", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to invite to team: %s", err) } @@ -133,12 +133,12 @@ func (api *Client) SetRegular(teamName, user string) error { func (api *Client) SetRegularContext(ctx context.Context, teamName, user string) error { values := url.Values{ "user": {user}, - "token": {api.config.token}, + "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "setRegular", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "setRegular", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err) } @@ -155,12 +155,12 @@ func (api *Client) SendSSOBindingEmail(teamName, user string) error { func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, user string) error { values := url.Values{ "user": {user}, - "token": {api.config.token}, + "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "sendSSOBind", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "sendSSOBind", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err) } @@ -178,12 +178,12 @@ func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid, values := url.Values{ "user": {uid}, "channel": {channel}, - "token": {api.config.token}, + "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "setUltraRestricted", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "setUltraRestricted", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to ultra-restrict account: %s", err) } @@ -200,12 +200,12 @@ func (api *Client) SetRestricted(teamName, uid string) error { func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid string) error { values := url.Values{ "user": {uid}, - "token": {api.config.token}, + "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "setRestricted", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "setRestricted", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to restrict account: %s", err) } diff --git a/vendor/github.com/nlopes/slack/attachments.go b/vendor/github.com/nlopes/slack/attachments.go index c5a66d9..d2b8b23 100644 --- a/vendor/github.com/nlopes/slack/attachments.go +++ b/vendor/github.com/nlopes/slack/attachments.go @@ -25,6 +25,7 @@ type AttachmentAction struct { SelectedOptions []AttachmentActionOption `json:"selected_options,omitempty"` // Optional. The first element of this array will be set as the pre-selected option for this menu. OptionGroups []AttachmentActionOptionGroup `json:"option_groups,omitempty"` // Optional. Confirm *ConfirmationField `json:"confirm,omitempty"` // Optional. + URL string `json:"url,omitempty"` // Optional. } // AttachmentActionOption the individual option to appear in action menu. @@ -48,6 +49,9 @@ type AttachmentActionCallback struct { 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"` @@ -55,6 +59,7 @@ type AttachmentActionCallback struct { AttachmentID string `json:"attachment_id"` Token string `json:"token"` ResponseURL string `json:"response_url"` + TriggerID string `json:"trigger_id"` } // ConfirmationField are used to ask users to confirm actions @@ -71,6 +76,7 @@ type Attachment struct { Fallback string `json:"fallback"` CallbackID string `json:"callback_id,omitempty"` + ID int `json:"id,omitempty"` AuthorName string `json:"author_name,omitempty"` AuthorSubname string `json:"author_subname,omitempty"` diff --git a/vendor/github.com/nlopes/slack/bots.go b/vendor/github.com/nlopes/slack/bots.go index 13a78cb..cb06088 100644 --- a/vendor/github.com/nlopes/slack/bots.go +++ b/vendor/github.com/nlopes/slack/bots.go @@ -19,9 +19,9 @@ type botResponseFull struct { SlackResponse } -func botRequest(ctx context.Context, path string, values url.Values, debug bool) (*botResponseFull, error) { +func botRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*botResponseFull, error) { response := &botResponseFull{} - err := post(ctx, path, values, response, debug) + err := post(ctx, client, path, values, response, debug) if err != nil { return nil, err } @@ -39,10 +39,11 @@ func (api *Client) GetBotInfo(bot string) (*Bot, error) { // GetBotInfoContext will retrieve the complete bot information using a custom context func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "bot": {bot}, } - response, err := botRequest(ctx, "bots.info", values, api.debug) + + response, err := botRequest(ctx, api.httpclient, "bots.info", values, api.debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/channels.go b/vendor/github.com/nlopes/slack/channels.go index 4a67b2f..b16e19f 100644 --- a/vendor/github.com/nlopes/slack/channels.go +++ b/vendor/github.com/nlopes/slack/channels.go @@ -20,14 +20,15 @@ type channelResponseFull struct { // Channel contains information about the channel type Channel struct { groupConversation - IsChannel bool `json:"is_channel"` - IsGeneral bool `json:"is_general"` - IsMember bool `json:"is_member"` + IsChannel bool `json:"is_channel"` + IsGeneral bool `json:"is_general"` + IsMember bool `json:"is_member"` + Locale string `json:"locale"` } -func channelRequest(ctx context.Context, path string, values url.Values, debug bool) (*channelResponseFull, error) { +func channelRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*channelResponseFull, error) { response := &channelResponseFull{} - err := post(ctx, path, values, response, debug) + err := postForm(ctx, client, SLACK_API+path, values, response, debug) if err != nil { return nil, err } @@ -38,53 +39,62 @@ func channelRequest(ctx context.Context, path string, values url.Values, debug b } // ArchiveChannel archives the given channel -func (api *Client) ArchiveChannel(channel string) error { - return api.ArchiveChannelContext(context.Background(), channel) +// see https://api.slack.com/methods/channels.archive +func (api *Client) ArchiveChannel(channelID string) error { + return api.ArchiveChannelContext(context.Background(), channelID) } // ArchiveChannelContext archives the given channel with a custom context -func (api *Client) ArchiveChannelContext(ctx context.Context, channel string) error { +// see https://api.slack.com/methods/channels.archive +func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) (err error) { values := url.Values{ - "token": {api.config.token}, - "channel": {channel}, + "token": {api.token}, + "channel": {channelID}, } - _, err := channelRequest(ctx, "channels.archive", values, api.debug) - if err != nil { + + if _, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug); err != nil { return err } + return nil } // UnarchiveChannel unarchives the given channel -func (api *Client) UnarchiveChannel(channel string) error { - return api.UnarchiveChannelContext(context.Background(), channel) +// see https://api.slack.com/methods/channels.unarchive +func (api *Client) UnarchiveChannel(channelID string) error { + return api.UnarchiveChannelContext(context.Background(), channelID) } // UnarchiveChannelContext unarchives the given channel with a custom context -func (api *Client) UnarchiveChannelContext(ctx context.Context, channel string) error { +// see https://api.slack.com/methods/channels.unarchive +func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string) (err error) { values := url.Values{ - "token": {api.config.token}, - "channel": {channel}, + "token": {api.token}, + "channel": {channelID}, } - _, err := channelRequest(ctx, "channels.unarchive", values, api.debug) - if err != nil { + + if _, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug); err != nil { return err } + return nil } // CreateChannel creates a channel with the given name and returns a *Channel -func (api *Client) CreateChannel(channel string) (*Channel, error) { - return api.CreateChannelContext(context.Background(), channel) +// see https://api.slack.com/methods/channels.create +func (api *Client) CreateChannel(channelName string) (*Channel, error) { + return api.CreateChannelContext(context.Background(), channelName) } // CreateChannelContext creates a channel with the given name and returns a *Channel with a custom context -func (api *Client) CreateChannelContext(ctx context.Context, channel string) (*Channel, error) { +// see https://api.slack.com/methods/channels.create +func (api *Client) CreateChannelContext(ctx context.Context, channelName string) (*Channel, error) { values := url.Values{ - "token": {api.config.token}, - "name": {channel}, + "token": {api.token}, + "name": {channelName}, } - response, err := channelRequest(ctx, "channels.create", values, api.debug) + + response, err := channelRequest(ctx, api.httpclient, "channels.create", values, api.debug) if err != nil { return nil, err } @@ -92,15 +102,17 @@ func (api *Client) CreateChannelContext(ctx context.Context, channel string) (*C } // GetChannelHistory retrieves the channel history -func (api *Client) GetChannelHistory(channel string, params HistoryParameters) (*History, error) { - return api.GetChannelHistoryContext(context.Background(), channel, params) +// see https://api.slack.com/methods/channels.history +func (api *Client) GetChannelHistory(channelID string, params HistoryParameters) (*History, error) { + return api.GetChannelHistoryContext(context.Background(), channelID, params) } // GetChannelHistoryContext retrieves the channel history with a custom context -func (api *Client) GetChannelHistoryContext(ctx context.Context, channel string, params HistoryParameters) (*History, error) { +// see https://api.slack.com/methods/channels.history +func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID string, params HistoryParameters) (*History, error) { values := url.Values{ - "token": {api.config.token}, - "channel": {channel}, + "token": {api.token}, + "channel": {channelID}, } if params.Latest != DEFAULT_HISTORY_LATEST { values.Add("latest", params.Latest) @@ -118,6 +130,7 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channel string, values.Add("inclusive", "0") } } + if params.Unreads != DEFAULT_HISTORY_UNREADS { if params.Unreads { values.Add("unreads", "1") @@ -125,7 +138,8 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channel string, values.Add("unreads", "0") } } - response, err := channelRequest(ctx, "channels.history", values, api.debug) + + response, err := channelRequest(ctx, api.httpclient, "channels.history", values, api.debug) if err != nil { return nil, err } @@ -133,17 +147,20 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channel string, } // GetChannelInfo retrieves the given channel -func (api *Client) GetChannelInfo(channel string) (*Channel, error) { - return api.GetChannelInfoContext(context.Background(), channel) +// see https://api.slack.com/methods/channels.info +func (api *Client) GetChannelInfo(channelID string) (*Channel, error) { + return api.GetChannelInfoContext(context.Background(), channelID) } // GetChannelInfoContext retrieves the given channel with a custom context -func (api *Client) GetChannelInfoContext(ctx context.Context, channel 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.config.token}, - "channel": {channel}, + "token": {api.token}, + "channel": {channelID}, } - response, err := channelRequest(ctx, "channels.info", values, api.debug) + + response, err := channelRequest(ctx, api.httpclient, "channels.info", values, api.debug) if err != nil { return nil, err } @@ -151,18 +168,21 @@ func (api *Client) GetChannelInfoContext(ctx context.Context, channel string) (* } // InviteUserToChannel invites a user to a given channel and returns a *Channel -func (api *Client) InviteUserToChannel(channel, user string) (*Channel, error) { - return api.InviteUserToChannelContext(context.Background(), channel, user) +// see https://api.slack.com/methods/channels.invite +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 -func (api *Client) InviteUserToChannelContext(ctx context.Context, channel, user string) (*Channel, error) { +// see https://api.slack.com/methods/channels.invite +func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) { values := url.Values{ - "token": {api.config.token}, - "channel": {channel}, + "token": {api.token}, + "channel": {channelID}, "user": {user}, } - response, err := channelRequest(ctx, "channels.invite", values, api.debug) + + response, err := channelRequest(ctx, api.httpclient, "channels.invite", values, api.debug) if err != nil { return nil, err } @@ -170,17 +190,20 @@ func (api *Client) InviteUserToChannelContext(ctx context.Context, channel, user } // JoinChannel joins the currently authenticated user to a channel -func (api *Client) JoinChannel(channel string) (*Channel, error) { - return api.JoinChannelContext(context.Background(), channel) +// see https://api.slack.com/methods/channels.join +func (api *Client) JoinChannel(channelName string) (*Channel, error) { + return api.JoinChannelContext(context.Background(), channelName) } // JoinChannelContext joins the currently authenticated user to a channel with a custom context -func (api *Client) JoinChannelContext(ctx context.Context, channel string) (*Channel, error) { +// see https://api.slack.com/methods/channels.join +func (api *Client) JoinChannelContext(ctx context.Context, channelName string) (*Channel, error) { values := url.Values{ - "token": {api.config.token}, - "name": {channel}, + "token": {api.token}, + "name": {channelName}, } - response, err := channelRequest(ctx, "channels.join", values, api.debug) + + response, err := channelRequest(ctx, api.httpclient, "channels.join", values, api.debug) if err != nil { return nil, err } @@ -188,59 +211,66 @@ func (api *Client) JoinChannelContext(ctx context.Context, channel string) (*Cha } // LeaveChannel makes the authenticated user leave the given channel -func (api *Client) LeaveChannel(channel string) (bool, error) { - return api.LeaveChannelContext(context.Background(), channel) +// see https://api.slack.com/methods/channels.leave +func (api *Client) LeaveChannel(channelID string) (bool, error) { + return api.LeaveChannelContext(context.Background(), channelID) } // LeaveChannelContext makes the authenticated user leave the given channel with a custom context -func (api *Client) LeaveChannelContext(ctx context.Context, channel string) (bool, error) { +// see https://api.slack.com/methods/channels.leave +func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (bool, error) { values := url.Values{ - "token": {api.config.token}, - "channel": {channel}, + "token": {api.token}, + "channel": {channelID}, } - response, err := channelRequest(ctx, "channels.leave", values, api.debug) + + response, err := channelRequest(ctx, api.httpclient, "channels.leave", values, api.debug) if err != nil { return false, err } - if response.NotInChannel { - return response.NotInChannel, nil - } - return false, nil + + return response.NotInChannel, nil } // KickUserFromChannel kicks a user from a given channel -func (api *Client) KickUserFromChannel(channel, user string) error { - return api.KickUserFromChannelContext(context.Background(), channel, user) +// see https://api.slack.com/methods/channels.kick +func (api *Client) KickUserFromChannel(channelID, user string) error { + return api.KickUserFromChannelContext(context.Background(), channelID, user) } // KickUserFromChannelContext kicks a user from a given channel with a custom context -func (api *Client) KickUserFromChannelContext(ctx context.Context, channel, user string) error { +// see https://api.slack.com/methods/channels.kick +func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, user string) (err error) { values := url.Values{ - "token": {api.config.token}, - "channel": {channel}, + "token": {api.token}, + "channel": {channelID}, "user": {user}, } - _, err := channelRequest(ctx, "channels.kick", values, api.debug) - if err != nil { + + if _, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug); err != nil { return err } + return nil } // 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) } // 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.config.token}, + "token": {api.token}, } if excludeArchived { values.Add("exclude_archived", "1") } - response, err := channelRequest(ctx, "channels.list", values, api.debug) + + response, err := channelRequest(ctx, api.httpclient, "channels.list", values, api.debug) if err != nil { return nil, err } @@ -252,40 +282,46 @@ func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool) // timer before making the call. In this way, any further updates needed during the timeout will not generate extra calls // (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A // timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. -func (api *Client) SetChannelReadMark(channel, ts string) error { - return api.SetChannelReadMarkContext(context.Background(), channel, ts) +// see https://api.slack.com/methods/channels.mark +func (api *Client) SetChannelReadMark(channelID, ts string) error { + return api.SetChannelReadMarkContext(context.Background(), channelID, ts) } // SetChannelReadMarkContext sets the read mark of a given channel to a specific point with a custom context // For more details see SetChannelReadMark documentation -func (api *Client) SetChannelReadMarkContext(ctx context.Context, channel, ts string) error { +// see https://api.slack.com/methods/channels.mark +func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts string) (err error) { values := url.Values{ - "token": {api.config.token}, - "channel": {channel}, + "token": {api.token}, + "channel": {channelID}, "ts": {ts}, } - _, err := channelRequest(ctx, "channels.mark", values, api.debug) - if err != nil { + + if _, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug); err != nil { return err } + return nil } // RenameChannel renames a given channel -func (api *Client) RenameChannel(channel, name string) (*Channel, error) { - return api.RenameChannelContext(context.Background(), channel, name) +// see https://api.slack.com/methods/channels.rename +func (api *Client) RenameChannel(channelID, name string) (*Channel, error) { + return api.RenameChannelContext(context.Background(), channelID, name) } // RenameChannelContext renames a given channel with a custom context -func (api *Client) RenameChannelContext(ctx context.Context, channel, name string) (*Channel, error) { +// see https://api.slack.com/methods/channels.rename +func (api *Client) RenameChannelContext(ctx context.Context, channelID, name string) (*Channel, error) { values := url.Values{ - "token": {api.config.token}, - "channel": {channel}, + "token": {api.token}, + "channel": {channelID}, "name": {name}, } + // 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, "channels.rename", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "channels.rename", values, api.debug) if err != nil { return nil, err } @@ -293,18 +329,21 @@ func (api *Client) RenameChannelContext(ctx context.Context, channel, name strin } // SetChannelPurpose sets the channel purpose and returns the purpose that was successfully set -func (api *Client) SetChannelPurpose(channel, purpose string) (string, error) { - return api.SetChannelPurposeContext(context.Background(), channel, purpose) +// see https://api.slack.com/methods/channels.setPurpose +func (api *Client) SetChannelPurpose(channelID, purpose string) (string, error) { + return api.SetChannelPurposeContext(context.Background(), channelID, purpose) } // SetChannelPurposeContext sets the channel purpose and returns the purpose that was successfully set with a custom context -func (api *Client) SetChannelPurposeContext(ctx context.Context, channel, purpose string) (string, error) { +// see https://api.slack.com/methods/channels.setPurpose +func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purpose string) (string, error) { values := url.Values{ - "token": {api.config.token}, - "channel": {channel}, + "token": {api.token}, + "channel": {channelID}, "purpose": {purpose}, } - response, err := channelRequest(ctx, "channels.setPurpose", values, api.debug) + + response, err := channelRequest(ctx, api.httpclient, "channels.setPurpose", values, api.debug) if err != nil { return "", err } @@ -312,18 +351,21 @@ func (api *Client) SetChannelPurposeContext(ctx context.Context, channel, purpos } // SetChannelTopic sets the channel topic and returns the topic that was successfully set -func (api *Client) SetChannelTopic(channel, topic string) (string, error) { - return api.SetChannelTopicContext(context.Background(), channel, topic) +// see https://api.slack.com/methods/channels.setTopic +func (api *Client) SetChannelTopic(channelID, topic string) (string, error) { + return api.SetChannelTopicContext(context.Background(), channelID, topic) } // SetChannelTopicContext sets the channel topic and returns the topic that was successfully set with a custom context -func (api *Client) SetChannelTopicContext(ctx context.Context, channel, topic string) (string, error) { +// see https://api.slack.com/methods/channels.setTopic +func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic string) (string, error) { values := url.Values{ - "token": {api.config.token}, - "channel": {channel}, + "token": {api.token}, + "channel": {channelID}, "topic": {topic}, } - response, err := channelRequest(ctx, "channels.setTopic", values, api.debug) + + response, err := channelRequest(ctx, api.httpclient, "channels.setTopic", values, api.debug) if err != nil { return "", err } @@ -331,18 +373,20 @@ func (api *Client) SetChannelTopicContext(ctx context.Context, channel, topic st } // GetChannelReplies gets an entire thread (a message plus all the messages in reply to it). -func (api *Client) GetChannelReplies(channel, thread_ts string) ([]Message, error) { - return api.GetChannelRepliesContext(context.Background(), channel, thread_ts) +// see https://api.slack.com/methods/channels.replies +func (api *Client) GetChannelReplies(channelID, thread_ts string) ([]Message, error) { + return api.GetChannelRepliesContext(context.Background(), channelID, thread_ts) } // GetChannelRepliesContext gets an entire thread (a message plus all the messages in reply to it) with a custom context -func (api *Client) GetChannelRepliesContext(ctx context.Context, channel, thread_ts string) ([]Message, error) { +// see https://api.slack.com/methods/channels.replies +func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thread_ts string) ([]Message, error) { values := url.Values{ - "token": {api.config.token}, - "channel": {channel}, + "token": {api.token}, + "channel": {channelID}, "thread_ts": {thread_ts}, } - response, err := channelRequest(ctx, "channels.replies", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "channels.replies", values, api.debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/chat.go b/vendor/github.com/nlopes/slack/chat.go index 9a2c445..fae416b 100644 --- a/vendor/github.com/nlopes/slack/chat.go +++ b/vendor/github.com/nlopes/slack/chat.go @@ -10,9 +10,10 @@ import ( const ( DEFAULT_MESSAGE_USERNAME = "" - DEFAULT_MESSAGE_THREAD_TIMESTAMP = "" + DEFAULT_MESSAGE_REPLY_BROADCAST = false DEFAULT_MESSAGE_ASUSER = false DEFAULT_MESSAGE_PARSE = "" + DEFAULT_MESSAGE_THREAD_TIMESTAMP = "" DEFAULT_MESSAGE_LINK_NAMES = 0 DEFAULT_MESSAGE_UNFURL_LINKS = false DEFAULT_MESSAGE_UNFURL_MEDIA = true @@ -31,11 +32,11 @@ type chatResponseFull struct { // PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request type PostMessageParameters struct { - Text string `json:"text"` Username string `json:"user_name"` 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"` @@ -44,22 +45,28 @@ type PostMessageParameters struct { IconEmoji string `json:"icon_emoji"` Markdown bool `json:"mrkdwn,omitempty"` EscapeText bool `json:"escape_text"` + + // chat.postEphemeral support + Channel string `json:"channel"` + User string `json:"user"` } // NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set func NewPostMessageParameters() PostMessageParameters { return PostMessageParameters{ - Username: DEFAULT_MESSAGE_USERNAME, - AsUser: DEFAULT_MESSAGE_ASUSER, - Parse: DEFAULT_MESSAGE_PARSE, - LinkNames: DEFAULT_MESSAGE_LINK_NAMES, - Attachments: nil, - UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS, - UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA, - IconURL: DEFAULT_MESSAGE_ICON_URL, - IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI, - Markdown: DEFAULT_MESSAGE_MARKDOWN, - EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT, + Username: DEFAULT_MESSAGE_USERNAME, + User: DEFAULT_MESSAGE_USERNAME, + AsUser: DEFAULT_MESSAGE_ASUSER, + 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, + IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI, + Markdown: DEFAULT_MESSAGE_MARKDOWN, + EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT, } } @@ -102,12 +109,43 @@ func (api *Client) PostMessageContext(ctx context.Context, channel, text string, return respChannel, respTimestamp, err } +// PostEphemeral sends an ephemeral message to a user in 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) PostEphemeral(channel, userID string, options ...MsgOption) (string, error) { + options = append(options, MsgOptionPostEphemeral()) + return api.PostEphemeralContext( + context.Background(), + channel, + userID, + options..., + ) +} + +// 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, channel, userID string, options ...MsgOption) (string, error) { + path, values, err := ApplyMsgOptions(api.token, channel, options...) + if err != nil { + return "", err + } + + values.Add("user", userID) + + response, err := chatRequest(ctx, api.httpclient, path, values, api.debug) + if err != nil { + return "", err + } + + return response.Timestamp, nil +} + // UpdateMessage updates a message in a channel func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) { return api.UpdateMessageContext(context.Background(), channel, timestamp, text) } -// UpdateMessage updates a message in a channel +// UpdateMessageContext updates a message in a channel func (api *Client) UpdateMessageContext(ctx context.Context, channel, timestamp, text string) (string, string, string, error) { return api.SendMessageContext(ctx, channel, MsgOptionUpdate(timestamp), MsgOptionText(text, true)) } @@ -119,12 +157,12 @@ func (api *Client) SendMessage(channel string, options ...MsgOption) (string, st // SendMessageContext more flexible method for configuring messages with a custom context. func (api *Client) SendMessageContext(ctx context.Context, channel string, options ...MsgOption) (string, string, string, error) { - channel, values, err := ApplyMsgOptions(api.config.token, channel, options...) + channel, values, err := ApplyMsgOptions(api.token, channel, options...) if err != nil { return "", "", "", err } - response, err := chatRequest(ctx, channel, values, api.debug) + response, err := chatRequest(ctx, api.httpclient, channel, values, api.debug) if err != nil { return "", "", "", err } @@ -156,9 +194,9 @@ func escapeMessage(message string) string { return replacer.Replace(message) } -func chatRequest(ctx context.Context, path string, values url.Values, debug bool) (*chatResponseFull, error) { +func chatRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*chatResponseFull, error) { response := &chatResponseFull{} - err := post(ctx, path, values, response, debug) + err := post(ctx, client, path, values, response, debug) if err != nil { return nil, err } @@ -171,9 +209,10 @@ func chatRequest(ctx context.Context, path string, values url.Values, debug bool type sendMode string const ( - chatUpdate sendMode = "chat.update" - chatPostMessage sendMode = "chat.postMessage" - chatDelete sendMode = "chat.delete" + chatUpdate sendMode = "chat.update" + chatPostMessage sendMode = "chat.postMessage" + chatDelete sendMode = "chat.delete" + chatPostEphemeral sendMode = "chat.postEphemeral" ) type sendConfig struct { @@ -193,6 +232,15 @@ func MsgOptionPost() MsgOption { } } +// MsgOptionPostEphemeral posts an ephemeral message +func MsgOptionPostEphemeral() MsgOption { + return func(config *sendConfig) error { + config.mode = chatPostEphemeral + config.values.Del("ts") + return nil + } +} + // MsgOptionUpdate updates a message based on the timestamp. func MsgOptionUpdate(timestamp string) MsgOption { return func(config *sendConfig) error { @@ -256,6 +304,14 @@ func MsgOptionEnableLinkUnfurl() MsgOption { } } +// MsgOptionDisableLinkUnfurl disables link unfurling +func MsgOptionDisableLinkUnfurl() MsgOption { + return func(config *sendConfig) error { + config.values.Set("unfurl_links", "false") + return nil + } +} + // MsgOptionDisableMediaUnfurl disables media unfurling. func MsgOptionDisableMediaUnfurl() MsgOption { return func(config *sendConfig) error { @@ -279,6 +335,11 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { config.values.Set("username", string(params.Username)) } + // chat.postEphemeral support + if params.User != DEFAULT_MESSAGE_USERNAME { + config.values.Set("user", params.User) + } + // never generates an error. MsgOptionAsUser(params.AsUser)(config) @@ -314,6 +375,9 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP { config.values.Set("thread_ts", params.ThreadTimestamp) } + if params.ReplyBroadcast != DEFAULT_MESSAGE_REPLY_BROADCAST { + config.values.Set("reply_broadcast", "true") + } return nil } diff --git a/vendor/github.com/nlopes/slack/conversation.go b/vendor/github.com/nlopes/slack/conversation.go index 83a1d4e..26ee292 100644 --- a/vendor/github.com/nlopes/slack/conversation.go +++ b/vendor/github.com/nlopes/slack/conversation.go @@ -1,5 +1,13 @@ package slack +import ( + "context" + "errors" + "net/url" + "strconv" + "strings" +) + // Conversation is the foundation for IM and BaseGroupConversation type conversation struct { ID string `json:"id"` @@ -9,6 +17,20 @@ type conversation struct { Latest *Message `json:"latest,omitempty"` UnreadCount int `json:"unread_count,omitempty"` UnreadCountDisplay int `json:"unread_count_display,omitempty"` + IsGroup bool `json:"is_group"` + IsShared bool `json:"is_shared"` + IsIM bool `json:"is_im"` + IsExtShared bool `json:"is_ext_shared"` + IsOrgShared bool `json:"is_org_shared"` + IsPendingExtShared bool `json:"is_pending_ext_shared"` + IsPrivate bool `json:"is_private"` + IsMpIM bool `json:"is_mpim"` + Unlinked int `json:"unlinked"` + NameNormalized string `json:"name_normalized"` + NumMembers int `json:"num_members"` + Priority float64 `json:"priority"` + // TODO support pending_shared + // TODO support previous_names } // GroupConversation is the foundation for Group and Channel @@ -35,3 +57,536 @@ type Purpose struct { Creator string `json:"creator"` LastSet JSONTime `json:"last_set"` } + +type GetUsersInConversationParameters struct { + ChannelID string + Cursor string + Limit int +} + +type responseMetaData struct { + NextCursor string `json:"next_cursor"` +} + +// GetUsersInConversation returns the list of users in a conversation +func (api *Client) GetUsersInConversation(params *GetUsersInConversationParameters) ([]string, string, error) { + return api.GetUsersInConversationContext(context.Background(), params) +} + +// GetUsersInConversationContext returns the list of users in a conversation with a custom context +func (api *Client) GetUsersInConversationContext(ctx context.Context, params *GetUsersInConversationParameters) ([]string, string, error) { + values := url.Values{ + "token": {api.token}, + "channel": {params.ChannelID}, + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", string(params.Limit)) + } + response := struct { + Members []string `json:"members"` + ResponseMetaData responseMetaData `json:"response_metadata"` + SlackResponse + }{} + err := post(ctx, api.httpclient, "conversations.members", values, &response, api.debug) + if err != nil { + return nil, "", err + } + if !response.Ok { + return nil, "", errors.New(response.Error) + } + return response.Members, response.ResponseMetaData.NextCursor, nil +} + +// ArchiveConversation archives a conversation +func (api *Client) ArchiveConversation(channelID string) error { + return api.ArchiveConversationContext(context.Background(), channelID) +} + +// ArchiveConversationContext archives a conversation with a custom context +func (api *Client) ArchiveConversationContext(ctx context.Context, channelID string) error { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + response := SlackResponse{} + err := post(ctx, api.httpclient, "conversations.archive", values, &response, api.debug) + if err != nil { + return err + } + if !response.Ok { + return errors.New(response.Error) + } + return nil +} + +// UnArchiveConversation reverses conversation archival +func (api *Client) UnArchiveConversation(channelID string) error { + return api.UnArchiveConversationContext(context.Background(), channelID) +} + +// UnArchiveConversationContext reverses conversation archival with a custom context +func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID string) error { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + response := SlackResponse{} + err := post(ctx, api.httpclient, "conversations.unarchive", values, &response, api.debug) + if err != nil { + return err + } + if !response.Ok { + return errors.New(response.Error) + } + return nil +} + +// SetTopicOfConversation sets the topic for a conversation +func (api *Client) SetTopicOfConversation(channelID, topic string) (*Channel, error) { + return api.SetTopicOfConversationContext(context.Background(), channelID, topic) +} + +// SetTopicOfConversationContext sets the topic for a conversation with a custom context +func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID, topic string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "topic": {topic}, + } + response := struct { + SlackResponse + Channel *Channel `json:"channel"` + }{} + err := post(ctx, api.httpclient, "conversations.setTopic", values, &response, api.debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response.Channel, nil +} + +// SetPurposeOfConversation sets the purpose for a conversation +func (api *Client) SetPurposeOfConversation(channelID, purpose string) (*Channel, error) { + return api.SetPurposeOfConversationContext(context.Background(), channelID, purpose) +} + +// SetPurposeOfConversationContext sets the purpose for a conversation with a custom context +func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelID, purpose string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "purpose": {purpose}, + } + response := struct { + SlackResponse + Channel *Channel `json:"channel"` + }{} + err := post(ctx, api.httpclient, "conversations.setPurpose", values, &response, api.debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response.Channel, nil +} + +// RenameConversation renames a conversation +func (api *Client) RenameConversation(channelID, channelName string) (*Channel, error) { + return api.RenameConversationContext(context.Background(), channelID, channelName) +} + +// RenameConversationContext renames a conversation with a custom context +func (api *Client) RenameConversationContext(ctx context.Context, channelID, channelName string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "name": {channelName}, + } + response := struct { + SlackResponse + Channel *Channel `json:"channel"` + }{} + err := post(ctx, api.httpclient, "conversations.rename", values, &response, api.debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response.Channel, nil +} + +// InviteUsersToConversation invites users to a channel +func (api *Client) InviteUsersToConversation(channelID string, users ...string) (*Channel, error) { + return api.InviteUsersToConversationContext(context.Background(), channelID, users...) +} + +// InviteUsersToConversationContext invites users to a channel with a custom context +func (api *Client) InviteUsersToConversationContext(ctx context.Context, channelID string, users ...string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "users": {strings.Join(users, ",")}, + } + response := struct { + SlackResponse + Channel *Channel `json:"channel"` + }{} + err := post(ctx, api.httpclient, "conversations.invite", values, &response, api.debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response.Channel, nil +} + +// KickUserFromConversation removes a user from a conversation +func (api *Client) KickUserFromConversation(channelID string, user string) error { + return api.KickUserFromConversationContext(context.Background(), channelID, user) +} + +// KickUserFromConversationContext removes a user from a conversation with a custom context +func (api *Client) KickUserFromConversationContext(ctx context.Context, channelID string, user string) error { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "user": {user}, + } + response := SlackResponse{} + err := post(ctx, api.httpclient, "conversations.kick", values, &response, api.debug) + if err != nil { + return err + } + if !response.Ok { + return errors.New(response.Error) + } + return nil +} + +// CloseConversation closes a direct message or multi-person direct message +func (api *Client) CloseConversation(channelID string) (noOp bool, alreadyClosed bool, err error) { + return api.CloseConversationContext(context.Background(), channelID) +} + +// CloseConversationContext closes a direct message or multi-person direct message with a custom context +func (api *Client) CloseConversationContext(ctx context.Context, channelID string) (noOp bool, alreadyClosed bool, err error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + response := struct { + SlackResponse + NoOp bool `json:"no_op"` + AlreadyClosed bool `json:"already_closed"` + }{} + + err = post(ctx, api.httpclient, "conversations.close", values, &response, api.debug) + if err != nil { + return false, false, err + } + if !response.Ok { + return false, false, errors.New(response.Error) + } + return response.NoOp, response.AlreadyClosed, nil +} + +// CreateConversation initiates a public or private channel-based conversation +func (api *Client) CreateConversation(channelName string, isPrivate bool) (*Channel, error) { + return api.CreateConversationContext(context.Background(), channelName, isPrivate) +} + +// CreateConversationContext initiates a public or private channel-based conversation with a custom context +func (api *Client) CreateConversationContext(ctx context.Context, channelName string, isPrivate bool) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "name": {channelName}, + "is_private": {strconv.FormatBool(isPrivate)}, + } + response, err := channelRequest( + ctx, api.httpclient, "conversations.create", values, api.debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return &response.Channel, nil +} + +// GetConversationInfo retrieves information about a conversation +func (api *Client) GetConversationInfo(channelID string, includeLocale bool) (*Channel, error) { + return api.GetConversationInfoContext(context.Background(), channelID, includeLocale) +} + +// GetConversationInfoContext retrieves information about a conversation with a custom context +func (api *Client) GetConversationInfoContext(ctx context.Context, channelID string, includeLocale bool) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "include_locale": {strconv.FormatBool(includeLocale)}, + } + response, err := channelRequest( + ctx, api.httpclient, "conversations.info", values, api.debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return &response.Channel, nil +} + +// LeaveConversation leaves a conversation +func (api *Client) LeaveConversation(channelID string) (bool, error) { + return api.LeaveConversationContext(context.Background(), channelID) +} + +// LeaveConversationContext leaves a conversation with a custom context +func (api *Client) LeaveConversationContext(ctx context.Context, channelID string) (bool, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + + response, err := channelRequest(ctx, api.httpclient, "conversations.leave", values, api.debug) + if err != nil { + return false, err + } + + return response.NotInChannel, nil +} + +type GetConversationRepliesParameters struct { + ChannelID string + Timestamp string + Cursor string + Inclusive bool + Latest string + Limit int + Oldest string +} + +// GetConversationReplies retrieves a thread of messages posted to a conversation +func (api *Client) GetConversationReplies(params *GetConversationRepliesParameters) (msgs []Message, hasMore bool, nextCursor string, err error) { + return api.GetConversationRepliesContext(context.Background(), params) +} + +// GetConversationRepliesContext retrieves a thread of messages posted to a conversation with a custom context +func (api *Client) GetConversationRepliesContext(ctx context.Context, params *GetConversationRepliesParameters) (msgs []Message, hasMore bool, nextCursor string, err error) { + values := url.Values{ + "token": {api.token}, + "channel": {params.ChannelID}, + "ts": {params.Timestamp}, + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Latest != "" { + values.Add("latest", params.Latest) + } + if params.Limit != 0 { + values.Add("limit", string(params.Limit)) + } + if params.Oldest != "" { + values.Add("oldest", params.Oldest) + } + if params.Inclusive { + values.Add("inclusive", "1") + } else { + values.Add("inclusive", "0") + } + response := struct { + SlackResponse + HasMore bool `json:"has_more"` + ResponseMetaData struct { + NextCursor string `json:"next_cursor"` + } `json:"response_metadata"` + Messages []Message `json:"messages"` + }{} + + err = post(ctx, api.httpclient, "conversations.replies", values, &response, api.debug) + if err != nil { + return nil, false, "", err + } + if !response.Ok { + return nil, false, "", errors.New(response.Error) + } + return response.Messages, response.HasMore, response.ResponseMetaData.NextCursor, nil +} + +type GetConversationsParameters struct { + Cursor string + ExcludeArchived string + Limit int + Types []string +} + +// GetConversations returns the list of channels in a Slack team +func (api *Client) GetConversations(params *GetConversationsParameters) (channels []Channel, nextCursor string, err error) { + return api.GetConversationsContext(context.Background(), params) +} + +// GetConversationsContext returns the list of channels in a Slack team with a custom context +func (api *Client) GetConversationsContext(ctx context.Context, params *GetConversationsParameters) (channels []Channel, nextCursor string, err error) { + values := url.Values{ + "token": {api.token}, + "exclude_archived": {params.ExcludeArchived}, + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", string(params.Limit)) + } + if params.Types != nil { + values.Add("types", strings.Join(params.Types, ",")) + } + response := struct { + Channels []Channel `json:"channels"` + ResponseMetaData responseMetaData `json:"response_metadata"` + SlackResponse + }{} + err = post(ctx, api.httpclient, "conversations.list", values, &response, api.debug) + if err != nil { + return nil, "", err + } + if !response.Ok { + return nil, "", errors.New(response.Error) + } + return response.Channels, response.ResponseMetaData.NextCursor, nil +} + +type OpenConversationParameters struct { + ChannelID string + ReturnIM bool + Users []string +} + +// OpenConversation opens or resumes a direct message or multi-person direct message +func (api *Client) OpenConversation(params *OpenConversationParameters) (*Channel, bool, bool, error) { + return api.OpenConversationContext(context.Background(), params) +} + +// OpenConversationContext opens or resumes a direct message or multi-person direct message with a custom context +func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConversationParameters) (*Channel, bool, bool, error) { + values := url.Values{ + "token": {api.token}, + "return_im": {strconv.FormatBool(params.ReturnIM)}, + } + if params.ChannelID != "" { + values.Add("channel", params.ChannelID) + } + if params.Users != nil { + values.Add("users", strings.Join(params.Users, ",")) + } + response := struct { + Channel *Channel `json:"channel"` + NoOp bool `json:"no_op"` + AlreadyOpen bool `json:"already_open"` + SlackResponse + }{} + err := post(ctx, api.httpclient, "conversations.open", values, &response, api.debug) + if err != nil { + return nil, false, false, err + } + if !response.Ok { + return nil, false, false, errors.New(response.Error) + } + return response.Channel, response.NoOp, response.AlreadyOpen, nil +} + +// JoinConversation joins an existing conversation +func (api *Client) JoinConversation(channelID string) (*Channel, string, []string, error) { + return api.JoinConversationContext(context.Background(), channelID) +} + +// JoinConversationContext joins an existing conversation with a custom context +func (api *Client) JoinConversationContext(ctx context.Context, channelID string) (*Channel, string, []string, error) { + values := url.Values{"token": {api.token}, "channel": {channelID}} + response := struct { + Channel *Channel `json:"channel"` + Warning string `json:"warning"` + ResponseMetaData *struct { + Warnings []string `json:"warnings"` + } `json:"response_metadata"` + SlackResponse + }{} + err := post(ctx, api.httpclient, "conversations.join", values, &response, api.debug) + if err != nil { + return nil, "", nil, err + } + if !response.Ok { + return nil, "", nil, errors.New(response.Error) + } + var warnings []string + if response.ResponseMetaData != nil { + warnings = response.ResponseMetaData.Warnings + } + return response.Channel, response.Warning, warnings, nil +} + +type GetConversationHistoryParameters struct { + ChannelID string + Cursor string + Inclusive bool + Latest string + Limit int + Oldest string +} + +type GetConversationHistoryResponse struct { + SlackResponse + HasMore bool `json:"has_more"` + PinCount int `json:"pin_count"` + Latest string `json:"latest"` + ResponseMetaData struct { + NextCursor string `json:"next_cursor"` + } `json:"response_metadata"` + Messages []Message `json:"messages"` +} + +// GetConversationHistory joins an existing conversation +func (api *Client) GetConversationHistory(params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) { + return api.GetConversationHistoryContext(context.Background(), params) +} + +// GetConversationHistoryContext joins an existing conversation with a custom context +func (api *Client) GetConversationHistoryContext(ctx context.Context, params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) { + values := url.Values{"token": {api.token}, "channel": {params.ChannelID}} + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Inclusive { + values.Add("inclusive", "1") + } else { + values.Add("inclusive", "0") + } + if params.Latest != "" { + values.Add("latest", params.Latest) + } + if params.Limit != 0 { + values.Add("limit", string(params.Limit)) + } + if params.Oldest != "" { + values.Add("oldest", params.Oldest) + } + + response := GetConversationHistoryResponse{} + + err := post(ctx, api.httpclient, "conversations.history", values, &response, api.debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return &response, nil +} diff --git a/vendor/github.com/nlopes/slack/dnd.go b/vendor/github.com/nlopes/slack/dnd.go index 4f1b322..ad8512b 100644 --- a/vendor/github.com/nlopes/slack/dnd.go +++ b/vendor/github.com/nlopes/slack/dnd.go @@ -36,9 +36,9 @@ type dndTeamInfoResponse struct { SlackResponse } -func dndRequest(ctx context.Context, path string, values url.Values, debug bool) (*dndResponseFull, error) { +func dndRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*dndResponseFull, error) { response := &dndResponseFull{} - err := post(ctx, path, values, response, debug) + err := post(ctx, client, path, values, response, debug) if err != nil { return nil, err } @@ -56,11 +56,12 @@ func (api *Client) EndDND() error { // EndDNDContext ends the user's scheduled Do Not Disturb session with a custom context func (api *Client) EndDNDContext(ctx context.Context) error { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } response := &SlackResponse{} - if err := post(ctx, "dnd.endDnd", values, response, api.debug); err != nil { + + if err := post(ctx, api.httpclient, "dnd.endDnd", values, response, api.debug); err != nil { return err } if !response.Ok { @@ -77,10 +78,10 @@ func (api *Client) EndSnooze() (*DNDStatus, error) { // EndSnoozeContext ends the current user's snooze mode with a custom context func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } - response, err := dndRequest(ctx, "dnd.endSnooze", values, api.debug) + response, err := dndRequest(ctx, api.httpclient, "dnd.endSnooze", values, api.debug) if err != nil { return nil, err } @@ -95,12 +96,13 @@ func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) { // GetDNDInfoContext provides information about a user's current Do Not Disturb settings with a custom context. func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDStatus, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if user != nil { values.Set("user", *user) } - response, err := dndRequest(ctx, "dnd.info", values, api.debug) + + response, err := dndRequest(ctx, api.httpclient, "dnd.info", values, api.debug) if err != nil { return nil, err } @@ -115,11 +117,12 @@ func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error) // GetDNDTeamInfoContext provides information about a user's current Do Not Disturb settings with a custom context. func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (map[string]DNDStatus, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "users": {strings.Join(users, ",")}, } response := &dndTeamInfoResponse{} - if err := post(ctx, "dnd.teamInfo", values, response, api.debug); err != nil { + + if err := post(ctx, api.httpclient, "dnd.teamInfo", values, response, api.debug); err != nil { return nil, err } if !response.Ok { @@ -139,10 +142,11 @@ func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) { // For more information see the SetSnooze docs func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "num_minutes": {strconv.Itoa(minutes)}, } - response, err := dndRequest(ctx, "dnd.setSnooze", values, api.debug) + + response, err := dndRequest(ctx, api.httpclient, "dnd.setSnooze", values, api.debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/emoji.go b/vendor/github.com/nlopes/slack/emoji.go index 5da9da4..c4b3c93 100644 --- a/vendor/github.com/nlopes/slack/emoji.go +++ b/vendor/github.com/nlopes/slack/emoji.go @@ -19,10 +19,11 @@ func (api *Client) GetEmoji() (map[string]string, error) { // GetEmojiContext retrieves all the emojis with a custom context func (api *Client) GetEmojiContext(ctx context.Context) (map[string]string, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } response := &emojiResponseFull{} - err := post(ctx, "emoji.list", values, response, api.debug) + + err := post(ctx, api.httpclient, "emoji.list", values, response, api.debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/files.go b/vendor/github.com/nlopes/slack/files.go index fc4b7e2..555d3a5 100644 --- a/vendor/github.com/nlopes/slack/files.go +++ b/vendor/github.com/nlopes/slack/files.go @@ -136,9 +136,9 @@ func NewGetFilesParameters() GetFilesParameters { } } -func fileRequest(ctx context.Context, path string, values url.Values, debug bool) (*fileResponseFull, error) { +func fileRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*fileResponseFull, error) { response := &fileResponseFull{} - err := post(ctx, path, values, response, debug) + err := postForm(ctx, client, SLACK_API+path, values, response, debug) if err != nil { return nil, err } @@ -156,12 +156,13 @@ func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment // GetFileInfoContext retrieves a file and related comments with a custom context func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, page int) (*File, []Comment, *Paging, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "file": {fileID}, "count": {strconv.Itoa(count)}, "page": {strconv.Itoa(page)}, } - response, err := fileRequest(ctx, "files.info", values, api.debug) + + response, err := fileRequest(ctx, api.httpclient, "files.info", values, api.debug) if err != nil { return nil, nil, nil, err } @@ -176,7 +177,7 @@ func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) // GetFilesContext retrieves all files according to the parameters given with a custom context func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameters) ([]File, *Paging, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if params.User != DEFAULT_FILES_USER { values.Add("user", params.User) @@ -199,7 +200,8 @@ func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameter if params.Page != DEFAULT_FILES_PAGE { values.Add("page", strconv.Itoa(params.Page)) } - response, err := fileRequest(ctx, "files.list", values, api.debug) + + response, err := fileRequest(ctx, api.httpclient, "files.list", values, api.debug) if err != nil { return nil, nil, err } @@ -221,7 +223,7 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam } response := &fileResponseFull{} values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if params.Filetype != "" { values.Add("filetype", params.Filetype) @@ -240,11 +242,11 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam } if params.Content != "" { values.Add("content", params.Content) - err = post(ctx, "files.upload", values, response, api.debug) + err = postForm(ctx, api.httpclient, SLACK_API+"files.upload", values, response, api.debug) } else if params.File != "" { - err = postLocalWithMultipartResponse(ctx, "files.upload", params.File, "file", values, response, api.debug) + err = postLocalWithMultipartResponse(ctx, api.httpclient, SLACK_API+"files.upload", params.File, "file", values, response, api.debug) } else if params.Reader != nil { - err = postWithMultipartResponse(ctx, "files.upload", params.Filename, "file", values, params.Reader, response, api.debug) + err = postWithMultipartResponse(ctx, api.httpclient, SLACK_API+"files.upload", params.Filename, "file", values, params.Reader, response, api.debug) } if err != nil { return nil, err @@ -255,23 +257,46 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam return &response.File, nil } +// DeleteFileComment deletes a file's comment +func (api *Client) DeleteFileComment(commentID, fileID string) error { + return api.DeleteFileCommentContext(context.Background(), fileID, commentID) +} + +// 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") + } + + values := url.Values{ + "token": {api.token}, + "file": {fileID}, + "id": {commentID}, + } + if _, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api.debug); err != nil { + return err + } + + return nil +} + // DeleteFile deletes a file func (api *Client) DeleteFile(fileID string) error { return api.DeleteFileContext(context.Background(), fileID) } // DeleteFileContext deletes a file with a custom context -func (api *Client) DeleteFileContext(ctx context.Context, fileID string) error { +func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "file": {fileID}, } - _, err := fileRequest(ctx, "files.delete", values, api.debug) - if err != nil { + + if _, err = fileRequest(ctx, api.httpclient, "files.delete", values, api.debug); err != nil { return err } - return nil + return nil } // RevokeFilePublicURL disables public/external sharing for a file @@ -282,10 +307,11 @@ func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) { // RevokeFilePublicURLContext disables public/external sharing for a file with a custom context func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string) (*File, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "file": {fileID}, } - response, err := fileRequest(ctx, "files.revokePublicURL", values, api.debug) + + response, err := fileRequest(ctx, api.httpclient, "files.revokePublicURL", values, api.debug) if err != nil { return nil, err } @@ -300,10 +326,11 @@ func (api *Client) ShareFilePublicURL(fileID string) (*File, []Comment, *Paging, // ShareFilePublicURLContext enabled public/external sharing for a file with a custom context func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) (*File, []Comment, *Paging, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "file": {fileID}, } - response, err := fileRequest(ctx, "files.sharedPublicURL", values, api.debug) + + response, err := fileRequest(ctx, api.httpclient, "files.sharedPublicURL", values, api.debug) if err != nil { return nil, nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/groups.go b/vendor/github.com/nlopes/slack/groups.go index 444666d..d0e7d91 100644 --- a/vendor/github.com/nlopes/slack/groups.go +++ b/vendor/github.com/nlopes/slack/groups.go @@ -28,9 +28,9 @@ type groupResponseFull struct { SlackResponse } -func groupRequest(ctx context.Context, path string, values url.Values, debug bool) (*groupResponseFull, error) { +func groupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*groupResponseFull, error) { response := &groupResponseFull{} - err := post(ctx, path, values, response, debug) + err := postForm(ctx, client, SLACK_API+path, values, response, debug) if err != nil { return nil, err } @@ -45,17 +45,18 @@ func (api *Client) ArchiveGroup(group string) error { return api.ArchiveGroupContext(context.Background(), group) } -// ArchiveGroup archives a private group +// ArchiveGroupContext archives a private group func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, } - _, err := groupRequest(ctx, "groups.archive", values, api.debug) + + _, err := groupRequest(ctx, api.httpclient, "groups.archive", values, api.debug) if err != nil { return err } - return nil + return err } // UnarchiveGroup unarchives a private group @@ -63,13 +64,14 @@ func (api *Client) UnarchiveGroup(group string) error { return api.UnarchiveGroupContext(context.Background(), group) } -// UnarchiveGroup unarchives a private group +// UnarchiveGroupContext unarchives a private group func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) error { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, } - _, err := groupRequest(ctx, "groups.unarchive", values, api.debug) + + _, err := groupRequest(ctx, api.httpclient, "groups.unarchive", values, api.debug) if err != nil { return err } @@ -81,13 +83,14 @@ func (api *Client) CreateGroup(group string) (*Group, error) { return api.CreateGroupContext(context.Background(), group) } -// CreateGroup creates a private group +// CreateGroupContext creates a private group func (api *Client) CreateGroupContext(ctx context.Context, group string) (*Group, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "name": {group}, } - response, err := groupRequest(ctx, "groups.create", values, api.debug) + + response, err := groupRequest(ctx, api.httpclient, "groups.create", values, api.debug) if err != nil { return nil, err } @@ -104,14 +107,15 @@ func (api *Client) CreateChildGroup(group string) (*Group, error) { return api.CreateChildGroupContext(context.Background(), group) } -// CreateChildGroup creates a new private group archiving the old one with a custom context +// CreateChildGroupContext creates a new private group archiving the old one with a custom context // For more information see CreateChildGroup func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (*Group, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, } - response, err := groupRequest(ctx, "groups.createChild", values, api.debug) + + response, err := groupRequest(ctx, api.httpclient, "groups.createChild", values, api.debug) if err != nil { return nil, err } @@ -126,10 +130,11 @@ func (api *Client) CloseGroup(group string) (bool, bool, error) { // 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.config.token}, + "token": {api.token}, "channel": {group}, } - response, err := imRequest(ctx, "groups.close", values, api.debug) + + response, err := imRequest(ctx, api.httpclient, "groups.close", values, api.debug) if err != nil { return false, false, err } @@ -144,7 +149,7 @@ func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*His // GetGroupHistoryContext fetches all the history for a private group with a custom context func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, params HistoryParameters) (*History, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, } if params.Latest != DEFAULT_HISTORY_LATEST { @@ -170,7 +175,8 @@ func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, par values.Add("unreads", "0") } } - response, err := groupRequest(ctx, "groups.history", values, api.debug) + + response, err := groupRequest(ctx, api.httpclient, "groups.history", values, api.debug) if err != nil { return nil, err } @@ -185,11 +191,12 @@ func (api *Client) InviteUserToGroup(group, user string) (*Group, bool, error) { // InviteUserToGroupContext invites a specific user to a private group with a custom context func (api *Client) InviteUserToGroupContext(ctx context.Context, group, user string) (*Group, bool, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, "user": {user}, } - response, err := groupRequest(ctx, "groups.invite", values, api.debug) + + response, err := groupRequest(ctx, api.httpclient, "groups.invite", values, api.debug) if err != nil { return nil, false, err } @@ -202,15 +209,16 @@ func (api *Client) LeaveGroup(group string) error { } // LeaveGroupContext makes authenticated user leave the group with a custom context -func (api *Client) LeaveGroupContext(ctx context.Context, group string) error { +func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, } - _, err := groupRequest(ctx, "groups.leave", values, api.debug) - if err != nil { + + if _, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug); err != nil { return err } + return nil } @@ -220,16 +228,17 @@ func (api *Client) KickUserFromGroup(group, user string) error { } // KickUserFromGroupContext kicks a user from a group with a custom context -func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user string) error { +func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user string) (err error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, "user": {user}, } - _, err := groupRequest(ctx, "groups.kick", values, api.debug) - if err != nil { + + if _, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug); err != nil { return err } + return nil } @@ -241,12 +250,13 @@ func (api *Client) GetGroups(excludeArchived bool) ([]Group, error) { // GetGroupsContext retrieves all groups with a custom context func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) ([]Group, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if excludeArchived { values.Add("exclude_archived", "1") } - response, err := groupRequest(ctx, "groups.list", values, api.debug) + + response, err := groupRequest(ctx, api.httpclient, "groups.list", values, api.debug) if err != nil { return nil, err } @@ -261,10 +271,11 @@ 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.config.token}, + "token": {api.token}, "channel": {group}, } - response, err := groupRequest(ctx, "groups.info", values, api.debug) + + response, err := groupRequest(ctx, api.httpclient, "groups.info", values, api.debug) if err != nil { return nil, err } @@ -282,16 +293,17 @@ func (api *Client) SetGroupReadMark(group, ts string) error { // SetGroupReadMarkContext sets the read mark on a private group with a custom context // For more details see SetGroupReadMark -func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string) error { +func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string) (err error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, "ts": {ts}, } - _, err := groupRequest(ctx, "groups.mark", values, api.debug) - if err != nil { + + if _, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug); err != nil { return err } + return nil } @@ -303,10 +315,11 @@ func (api *Client) OpenGroup(group string) (bool, bool, error) { // OpenGroupContext opens a private group with a custom context func (api *Client) OpenGroupContext(ctx context.Context, group string) (bool, bool, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, } - response, err := groupRequest(ctx, "groups.open", values, api.debug) + + response, err := groupRequest(ctx, api.httpclient, "groups.open", values, api.debug) if err != nil { return false, false, err } @@ -323,13 +336,14 @@ func (api *Client) RenameGroup(group, name string) (*Channel, error) { // RenameGroupContext renames a group with a custom context func (api *Client) RenameGroupContext(ctx context.Context, group, name string) (*Channel, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, "name": {name}, } + // 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, "groups.rename", values, api.debug) + response, err := groupRequest(ctx, api.httpclient, "groups.rename", values, api.debug) if err != nil { return nil, err } @@ -344,11 +358,12 @@ func (api *Client) SetGroupPurpose(group, purpose string) (string, error) { // SetGroupPurposeContext sets the group purpose with a custom context func (api *Client) SetGroupPurposeContext(ctx context.Context, group, purpose string) (string, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, "purpose": {purpose}, } - response, err := groupRequest(ctx, "groups.setPurpose", values, api.debug) + + response, err := groupRequest(ctx, api.httpclient, "groups.setPurpose", values, api.debug) if err != nil { return "", err } @@ -363,11 +378,12 @@ func (api *Client) SetGroupTopic(group, topic string) (string, error) { // SetGroupTopicContext sets the group topic with a custom context func (api *Client) SetGroupTopicContext(ctx context.Context, group, topic string) (string, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, "topic": {topic}, } - response, err := groupRequest(ctx, "groups.setTopic", values, api.debug) + + response, err := groupRequest(ctx, api.httpclient, "groups.setTopic", values, api.debug) if err != nil { return "", err } diff --git a/vendor/github.com/nlopes/slack/im.go b/vendor/github.com/nlopes/slack/im.go index 0cbc8d3..55b24b7 100644 --- a/vendor/github.com/nlopes/slack/im.go +++ b/vendor/github.com/nlopes/slack/im.go @@ -29,9 +29,9 @@ type IM struct { IsUserDeleted bool `json:"is_user_deleted"` } -func imRequest(ctx context.Context, path string, values url.Values, debug bool) (*imResponseFull, error) { +func imRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*imResponseFull, error) { response := &imResponseFull{} - err := post(ctx, path, values, response, debug) + err := post(ctx, client, path, values, response, debug) if err != nil { return nil, err } @@ -49,10 +49,11 @@ func (api *Client) CloseIMChannel(channel string) (bool, bool, error) { // CloseIMChannelContext closes the direct message channel with a custom context func (api *Client) CloseIMChannelContext(ctx context.Context, channel string) (bool, bool, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channel}, } - response, err := imRequest(ctx, "im.close", values, api.debug) + + response, err := imRequest(ctx, api.httpclient, "im.close", values, api.debug) if err != nil { return false, false, err } @@ -69,10 +70,11 @@ func (api *Client) OpenIMChannel(user string) (bool, bool, string, error) { // Returns some status and the channel ID func (api *Client) OpenIMChannelContext(ctx context.Context, user string) (bool, bool, string, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "user": {user}, } - response, err := imRequest(ctx, "im.open", values, api.debug) + + response, err := imRequest(ctx, api.httpclient, "im.open", values, api.debug) if err != nil { return false, false, "", err } @@ -87,11 +89,12 @@ func (api *Client) MarkIMChannel(channel, ts string) (err error) { // MarkIMChannelContext sets the read mark of a direct message channel to a specific point with a custom context func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string) (err error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channel}, "ts": {ts}, } - _, err = imRequest(ctx, "im.mark", values, api.debug) + + _, err = imRequest(ctx, api.httpclient, "im.mark", values, api.debug) if err != nil { return err } @@ -106,7 +109,7 @@ func (api *Client) GetIMHistory(channel string, params HistoryParameters) (*Hist // GetIMHistoryContext retrieves the direct message channel history with a custom context func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, params HistoryParameters) (*History, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channel}, } if params.Latest != DEFAULT_HISTORY_LATEST { @@ -132,7 +135,8 @@ func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, para values.Add("unreads", "0") } } - response, err := imRequest(ctx, "im.history", values, api.debug) + + response, err := imRequest(ctx, api.httpclient, "im.history", values, api.debug) if err != nil { return nil, err } @@ -147,9 +151,10 @@ func (api *Client) GetIMChannels() ([]IM, error) { // GetIMChannelsContext returns the list of direct message channels with a custom context func (api *Client) GetIMChannelsContext(ctx context.Context) ([]IM, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } - response, err := imRequest(ctx, "im.list", values, api.debug) + + response, err := imRequest(ctx, api.httpclient, "im.list", values, api.debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/logger.go b/vendor/github.com/nlopes/slack/logger.go new file mode 100644 index 0000000..501d167 --- /dev/null +++ b/vendor/github.com/nlopes/slack/logger.go @@ -0,0 +1,53 @@ +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 { + Output(int, string) error +} + +// logInternal represents the internal logging api we use. +type logInternal interface { + 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 +} + +// Println replicates the behaviour of the standard logger. +func (t ilogger) 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{}) { + t.Output(2, fmt.Sprintf(format, v...)) +} + +// Print replicates the behaviour of the standard logger. +func (t ilogger) Print(v ...interface{}) { + t.Output(2, fmt.Sprint(v...)) +} diff --git a/vendor/github.com/nlopes/slack/messages.go b/vendor/github.com/nlopes/slack/messages.go index 39f0d6b..4d9df61 100644 --- a/vendor/github.com/nlopes/slack/messages.go +++ b/vendor/github.com/nlopes/slack/messages.go @@ -2,7 +2,8 @@ package slack // OutgoingMessage is used for the realtime API, and seems incomplete. type OutgoingMessage struct { - ID int `json:"id"` + ID int `json:"id"` + // channel ID Channel string `json:"channel,omitempty"` Text string `json:"text,omitempty"` Type string `json:"type,omitempty"` @@ -28,6 +29,9 @@ type Msg struct { PinnedTo []string `json:"pinned_to, omitempty"` Attachments []Attachment `json:"attachments,omitempty"` Edited *Edited `json:"edited,omitempty"` + LastRead string `json:"last_read,omitempty"` + Subscribed bool `json:"subscribed,omitempty"` + UnreadCount int `json:"unread_count,omitempty"` // Message Subtypes SubType string `json:"subtype,omitempty"` @@ -81,6 +85,10 @@ type Msg struct { // reactions Reactions []ItemReaction `json:"reactions,omitempty"` + + // slash commands and interactive messages + ResponseType string `json:"response_type,omitempty"` + ReplaceOriginal bool `json:"replace_original,omitempty"` } // Icon is used for bot messages @@ -121,12 +129,12 @@ type Pong struct { // NewOutgoingMessage prepares an OutgoingMessage that the user can // use to send a message. Use this function to properly set the // messageID. -func (rtm *RTM) NewOutgoingMessage(text string, channel string) *OutgoingMessage { +func (rtm *RTM) NewOutgoingMessage(text string, channelID string) *OutgoingMessage { id := rtm.idGen.Next() return &OutgoingMessage{ ID: id, Type: "message", - Channel: channel, + Channel: channelID, Text: text, } } @@ -134,11 +142,11 @@ func (rtm *RTM) NewOutgoingMessage(text string, channel string) *OutgoingMessage // NewTypingMessage prepares an OutgoingMessage that the user can // use to send as a typing indicator. Use this function to properly set the // messageID. -func (rtm *RTM) NewTypingMessage(channel string) *OutgoingMessage { +func (rtm *RTM) NewTypingMessage(channelID string) *OutgoingMessage { id := rtm.idGen.Next() return &OutgoingMessage{ ID: id, Type: "typing", - Channel: channel, + Channel: channelID, } } diff --git a/vendor/github.com/nlopes/slack/misc.go b/vendor/github.com/nlopes/slack/misc.go index 3a9ed2d..32f2367 100644 --- a/vendor/github.com/nlopes/slack/misc.go +++ b/vendor/github.com/nlopes/slack/misc.go @@ -13,24 +13,11 @@ import ( "net/url" "os" "path/filepath" + "strconv" "strings" "time" ) -// 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 { - Do(*http.Request) (*http.Response, error) -} - -var customHTTPClient HTTPRequester - -// HTTPClient sets a custom http.Client -// deprecated: in favor of SetHTTPClient() -var HTTPClient = &http.Client{} - type WebResponse struct { Ok bool `json:"ok"` Error *WebError `json:"error"` @@ -42,6 +29,14 @@ func (s WebError) Error() string { return string(s) } +type RateLimitedError struct { + RetryAfter time.Duration +} + +func (e *RateLimitedError) Error() string { + 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) @@ -79,15 +74,10 @@ func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error logger.Printf("parseResponseBody: %s\n", string(response)) } - err = json.Unmarshal(response, &intf) - if err != nil { - return err - } - - return nil + return json.Unmarshal(response, &intf) } -func postLocalWithMultipartResponse(ctx context.Context, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error { +func postLocalWithMultipartResponse(ctx context.Context, client HTTPRequester, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error { fullpath, err := filepath.Abs(fpath) if err != nil { return err @@ -97,23 +87,31 @@ func postLocalWithMultipartResponse(ctx context.Context, path, fpath, fieldname return err } defer file.Close() - return postWithMultipartResponse(ctx, path, filepath.Base(fpath), fieldname, values, file, intf, debug) + return postWithMultipartResponse(ctx, client, path, filepath.Base(fpath), fieldname, values, file, intf, debug) } -func postWithMultipartResponse(ctx context.Context, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, debug bool) error { +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) if err != nil { return err } req = req.WithContext(ctx) - resp, err := getHTTPClient().Do(req) + resp, err := client.Do(req) 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 != 200 { + if resp.StatusCode != http.StatusOK { logResponse(resp, debug) return fmt.Errorf("Slack server error: %s.", resp.Status) } @@ -121,7 +119,7 @@ func postWithMultipartResponse(ctx context.Context, path, name, fieldname string return parseResponseBody(resp.Body, &intf, debug) } -func postForm(ctx context.Context, endpoint string, values url.Values, intf interface{}, debug bool) error { +func postForm(ctx context.Context, client HTTPRequester, endpoint string, values url.Values, intf interface{}, debug bool) error { reqBody := strings.NewReader(values.Encode()) req, err := http.NewRequest("POST", endpoint, reqBody) if err != nil { @@ -130,14 +128,22 @@ func postForm(ctx context.Context, endpoint string, values url.Values, intf inte req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req = req.WithContext(ctx) - resp, err := getHTTPClient().Do(req) + resp, err := client.Do(req) 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 != 200 { + if resp.StatusCode != http.StatusOK { logResponse(resp, debug) return fmt.Errorf("Slack server error: %s.", resp.Status) } @@ -145,13 +151,13 @@ func postForm(ctx context.Context, endpoint string, values url.Values, intf inte return parseResponseBody(resp.Body, &intf, debug) } -func post(ctx context.Context, path string, values url.Values, intf interface{}, debug bool) error { - return postForm(ctx, SLACK_API+path, values, intf, debug) +func post(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 parseAdminResponse(ctx context.Context, method string, teamName string, values url.Values, intf interface{}, debug bool) error { +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, endpoint, values, intf, debug) + return postForm(ctx, client, endpoint, values, intf, debug) } func logResponse(resp *http.Response, debug bool) error { @@ -167,17 +173,10 @@ func logResponse(resp *http.Response, debug bool) error { return nil } -func getHTTPClient() HTTPRequester { - if customHTTPClient != nil { - return customHTTPClient - } - - return HTTPClient -} - -// 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 +func okJsonHandler(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + response, _ := json.Marshal(SlackResponse{ + Ok: true, + }) + rw.Write(response) } diff --git a/vendor/github.com/nlopes/slack/oauth.go b/vendor/github.com/nlopes/slack/oauth.go index db10aa1..0af190e 100644 --- a/vendor/github.com/nlopes/slack/oauth.go +++ b/vendor/github.com/nlopes/slack/oauth.go @@ -55,7 +55,7 @@ func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code, "redirect_uri": {redirectURI}, } response := &OAuthResponse{} - err = post(ctx, "oauth.access", values, response, debug) + err = post(ctx, customHTTPClient, "oauth.access", values, response, debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/pins.go b/vendor/github.com/nlopes/slack/pins.go index a20f8f7..6b39778 100644 --- a/vendor/github.com/nlopes/slack/pins.go +++ b/vendor/github.com/nlopes/slack/pins.go @@ -21,7 +21,7 @@ func (api *Client) AddPin(channel string, item ItemRef) error { func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemRef) error { values := url.Values{ "channel": {channel}, - "token": {api.config.token}, + "token": {api.token}, } if item.Timestamp != "" { values.Set("timestamp", string(item.Timestamp)) @@ -32,8 +32,9 @@ func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemR if item.Comment != "" { values.Set("file_comment", string(item.Comment)) } + response := &SlackResponse{} - if err := post(ctx, "pins.add", values, response, api.debug); err != nil { + if err := post(ctx, api.httpclient, "pins.add", values, response, api.debug); err != nil { return err } if !response.Ok { @@ -51,7 +52,7 @@ func (api *Client) RemovePin(channel string, item ItemRef) error { func (api *Client) RemovePinContext(ctx context.Context, channel string, item ItemRef) error { values := url.Values{ "channel": {channel}, - "token": {api.config.token}, + "token": {api.token}, } if item.Timestamp != "" { values.Set("timestamp", string(item.Timestamp)) @@ -62,8 +63,9 @@ func (api *Client) RemovePinContext(ctx context.Context, channel string, item It if item.Comment != "" { values.Set("file_comment", string(item.Comment)) } + response := &SlackResponse{} - if err := post(ctx, "pins.remove", values, response, api.debug); err != nil { + if err := post(ctx, api.httpclient, "pins.remove", values, response, api.debug); err != nil { return err } if !response.Ok { @@ -81,10 +83,11 @@ func (api *Client) ListPins(channel string) ([]Item, *Paging, error) { func (api *Client) ListPinsContext(ctx context.Context, channel string) ([]Item, *Paging, error) { values := url.Values{ "channel": {channel}, - "token": {api.config.token}, + "token": {api.token}, } + response := &listPinsResponseFull{} - err := post(ctx, "pins.list", values, response, api.debug) + err := post(ctx, api.httpclient, "pins.list", values, response, api.debug) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/reactions.go b/vendor/github.com/nlopes/slack/reactions.go index 9da5924..c0556d8 100644 --- a/vendor/github.com/nlopes/slack/reactions.go +++ b/vendor/github.com/nlopes/slack/reactions.go @@ -136,7 +136,7 @@ func (api *Client) AddReaction(name string, item ItemRef) error { // AddReactionContext adds a reaction emoji to a message, file or file comment with a custom context. func (api *Client) AddReactionContext(ctx context.Context, name string, item ItemRef) error { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if name != "" { values.Set("name", name) @@ -153,8 +153,9 @@ func (api *Client) AddReactionContext(ctx context.Context, name string, item Ite if item.Comment != "" { values.Set("file_comment", string(item.Comment)) } + response := &SlackResponse{} - if err := post(ctx, "reactions.add", values, response, api.debug); err != nil { + if err := post(ctx, api.httpclient, "reactions.add", values, response, api.debug); err != nil { return err } if !response.Ok { @@ -171,7 +172,7 @@ func (api *Client) RemoveReaction(name string, item ItemRef) error { // RemoveReactionContext removes a reaction emoji from a message, file or file comment with a custom context. func (api *Client) RemoveReactionContext(ctx context.Context, name string, item ItemRef) error { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if name != "" { values.Set("name", name) @@ -188,8 +189,9 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item if item.Comment != "" { values.Set("file_comment", string(item.Comment)) } + response := &SlackResponse{} - if err := post(ctx, "reactions.remove", values, response, api.debug); err != nil { + if err := post(ctx, api.httpclient, "reactions.remove", values, response, api.debug); err != nil { return err } if !response.Ok { @@ -206,7 +208,7 @@ func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([] // GetReactionsContext returns details about the reactions on an item with a custom context func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if item.Channel != "" { values.Set("channel", string(item.Channel)) @@ -223,8 +225,9 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params if params.Full != DEFAULT_REACTIONS_FULL { values.Set("full", strconv.FormatBool(params.Full)) } + response := &getReactionsResponseFull{} - if err := post(ctx, "reactions.get", values, response, api.debug); err != nil { + if err := post(ctx, api.httpclient, "reactions.get", values, response, api.debug); err != nil { return nil, err } if !response.Ok { @@ -241,7 +244,7 @@ func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, // ListReactionsContext returns information about the items a user reacted to with a custom context. func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, *Paging, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if params.User != DEFAULT_REACTIONS_USER { values.Add("user", params.User) @@ -255,8 +258,9 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction if params.Full != DEFAULT_REACTIONS_FULL { values.Add("full", strconv.FormatBool(params.Full)) } + response := &listReactionsResponseFull{} - err := post(ctx, "reactions.list", values, response, api.debug) + err := post(ctx, api.httpclient, "reactions.list", values, response, api.debug) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/rtm.go b/vendor/github.com/nlopes/slack/rtm.go index fd5d200..7b55c2a 100644 --- a/vendor/github.com/nlopes/slack/rtm.go +++ b/vendor/github.com/nlopes/slack/rtm.go @@ -8,11 +8,18 @@ import ( "time" ) +const ( + websocketDefaultTimeout = 10 * time.Second +) + // StartRTM calls the "rtm.start" endpoint and returns the provided URL and the full Info block. // // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. func (api *Client) StartRTM() (info *Info, websocketURL string, err error) { - return api.StartRTMContext(context.Background()) + ctx, cancel := context.WithTimeout(context.Background(), websocketDefaultTimeout) + defer cancel() + + return api.StartRTMContext(ctx) } // StartRTMContext calls the "rtm.start" endpoint and returns the provided URL and the full Info block with a custom context. @@ -20,31 +27,25 @@ 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 = post(ctx, "rtm.start", url.Values{"token": {api.config.token}}, response, api.debug) + err = post(ctx, api.httpclient, "rtm.start", url.Values{"token": {api.token}}, response, api.debug) if err != nil { return nil, "", fmt.Errorf("post: %s", err) } if !response.Ok { return nil, "", response.Error } - - // websocket.Dial does not accept url without the port (yet) - // Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3 - // but slack returns the address with no port, so we have to fix it api.Debugln("Using URL:", response.Info.URL) - websocketURL, err = websocketizeURLPort(response.Info.URL) - if err != nil { - return nil, "", fmt.Errorf("parsing response URL: %s", err) - } - - return &response.Info, websocketURL, nil + return &response.Info, response.Info.URL, nil } // ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block. // // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) { - return api.ConnectRTMContext(context.Background()) + ctx, cancel := context.WithTimeout(context.Background(), websocketDefaultTimeout) + defer cancel() + + return api.ConnectRTMContext(ctx) } // ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block with a custom context. @@ -52,24 +53,16 @@ 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 = post(ctx, "rtm.connect", url.Values{"token": {api.config.token}}, response, api.debug) + err = post(ctx, api.httpclient, "rtm.connect", url.Values{"token": {api.token}}, response, api.debug) if err != nil { + api.Debugf("Failed to connect to RTM: %s", err) return nil, "", fmt.Errorf("post: %s", err) } if !response.Ok { return nil, "", response.Error } - - // websocket.Dial does not accept url without the port (yet) - // Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3 - // but slack returns the address with no port, so we have to fix it api.Debugln("Using URL:", response.Info.URL) - websocketURL, err = websocketizeURLPort(response.Info.URL) - if err != nil { - return nil, "", fmt.Errorf("parsing response URL: %s", err) - } - - return &response.Info, websocketURL, nil + return &response.Info, response.Info.URL, nil } // NewRTM returns a RTM, which provides a fully managed connection to @@ -90,6 +83,7 @@ func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM { isConnected: false, wasIntentional: true, killChannel: make(chan bool), + disconnected: make(chan struct{}), forcePing: make(chan bool), rawEvents: make(chan json.RawMessage), idGen: NewSafeID(1), diff --git a/vendor/github.com/nlopes/slack/search.go b/vendor/github.com/nlopes/slack/search.go index 0e8d65e..390dcdb 100644 --- a/vendor/github.com/nlopes/slack/search.go +++ b/vendor/github.com/nlopes/slack/search.go @@ -83,7 +83,7 @@ func NewSearchParameters() SearchParameters { func (api *Client) _search(ctx context.Context, path, query string, params SearchParameters, files, messages bool) (response *searchResponseFull, error error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "query": {query}, } if params.Sort != DEFAULT_SEARCH_SORT { @@ -101,8 +101,9 @@ func (api *Client) _search(ctx context.Context, path, query string, params Searc if params.Page != DEFAULT_SEARCH_PAGE { values.Add("page", strconv.Itoa(params.Page)) } + response = &searchResponseFull{} - err := post(ctx, path, values, response, api.debug) + err := post(ctx, api.httpclient, path, values, response, api.debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/slack.go b/vendor/github.com/nlopes/slack/slack.go index a13bed3..ddf42e9 100644 --- a/vendor/github.com/nlopes/slack/slack.go +++ b/vendor/github.com/nlopes/slack/slack.go @@ -3,18 +3,38 @@ package slack import ( "context" "errors" + "fmt" "log" + "net/http" "net/url" "os" ) -var logger *log.Logger // A logger that can be set by consumers -/* - Added as a var so that we can change this for testing purposes -*/ +// 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" +// 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 { + 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 +} + type SlackResponse struct { Ok bool `json:"ok"` Error string `json:"error"` @@ -34,22 +54,33 @@ type authTestResponseFull struct { } type Client struct { - config struct { - token string + token string + info Info + debug bool + httpclient HTTPRequester +} + +// 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 } - info Info - debug bool } -// 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 *log.Logger) { - logger = l -} +// New builds a slack client from the provided token and options. +func New(token string, options ...Option) *Client { + s := &Client{ + token: token, + httpclient: customHTTPClient, + } + + for _, opt := range options { + opt(s) + } -func New(token string) *Client { - s := &Client{} - s.config.token = token return s } @@ -60,14 +91,19 @@ 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) { + api.Debugf("Challenging auth...") responseFull := &authTestResponseFull{} - err := post(ctx, "auth.test", url.Values{"token": {api.config.token}}, responseFull, api.debug) + err := post(ctx, api.httpclient, "auth.test", url.Values{"token": {api.token}}, responseFull, api.debug) 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 } @@ -77,18 +113,20 @@ func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestRespo func (api *Client) SetDebug(debug bool) { api.debug = debug if debug && logger == nil { - logger = log.New(os.Stdout, "nlopes/slack", log.LstdFlags|log.Lshortfile) + SetLogger(log.New(os.Stdout, "nlopes/slack", log.LstdFlags|log.Lshortfile)) } } +// Debugf print a formatted debug line. func (api *Client) Debugf(format string, v ...interface{}) { if api.debug { - logger.Printf(format, v...) + logger.Output(2, fmt.Sprintf(format, v...)) } } +// Debugln print a debug line. func (api *Client) Debugln(v ...interface{}) { if api.debug { - logger.Println(v...) + logger.Output(2, fmt.Sprintln(v...)) } } diff --git a/vendor/github.com/nlopes/slack/slash.go b/vendor/github.com/nlopes/slack/slash.go new file mode 100644 index 0000000..c21a478 --- /dev/null +++ b/vendor/github.com/nlopes/slack/slash.go @@ -0,0 +1,49 @@ +package slack + +import ( + "net/http" +) + +// SlashCommand contains information about a request of the slash command +type SlashCommand struct { + Token string `json:"token"` + TeamID string `json:"team_id"` + TeamDomain string `json:"team_domain"` + ChannelID string `json:"channel_id"` + ChannelName string `json:"channel_name"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` + Command string `json:"command"` + Text string `json:"text"` + ResponseURL string `json:"response_url"` + TriggerID string `json:"trigger_id"` +} + +// SlashCommandParse will parse the request of the slash command +func SlashCommandParse(r *http.Request) (s SlashCommand, err error) { + if err = r.ParseForm(); err != nil { + return s, err + } + s.Token = r.PostForm.Get("token") + s.TeamID = r.PostForm.Get("team_id") + s.TeamDomain = r.PostForm.Get("team_domain") + s.ChannelID = r.PostForm.Get("channel_id") + s.ChannelName = r.PostForm.Get("channel_name") + s.UserID = r.PostForm.Get("user_id") + s.UserName = r.PostForm.Get("user_name") + s.Command = r.PostForm.Get("command") + s.Text = r.PostForm.Get("text") + s.ResponseURL = r.PostForm.Get("response_url") + s.TriggerID = r.PostForm.Get("trigger_id") + return s, nil +} + +// ValidateToken validates verificationTokens +func (s SlashCommand) ValidateToken(verificationTokens ...string) bool { + for _, token := range verificationTokens { + if s.Token == token { + return true + } + } + return false +} diff --git a/vendor/github.com/nlopes/slack/stars.go b/vendor/github.com/nlopes/slack/stars.go index cf4a4a1..785dec5 100644 --- a/vendor/github.com/nlopes/slack/stars.go +++ b/vendor/github.com/nlopes/slack/stars.go @@ -45,7 +45,7 @@ func (api *Client) AddStar(channel string, item ItemRef) error { func (api *Client) AddStarContext(ctx context.Context, channel string, item ItemRef) error { values := url.Values{ "channel": {channel}, - "token": {api.config.token}, + "token": {api.token}, } if item.Timestamp != "" { values.Set("timestamp", string(item.Timestamp)) @@ -56,8 +56,9 @@ func (api *Client) AddStarContext(ctx context.Context, channel string, item Item if item.Comment != "" { values.Set("file_comment", string(item.Comment)) } + response := &SlackResponse{} - if err := post(ctx, "stars.add", values, response, api.debug); err != nil { + if err := post(ctx, api.httpclient, "stars.add", values, response, api.debug); err != nil { return err } if !response.Ok { @@ -75,7 +76,7 @@ func (api *Client) RemoveStar(channel string, item ItemRef) error { func (api *Client) RemoveStarContext(ctx context.Context, channel string, item ItemRef) error { values := url.Values{ "channel": {channel}, - "token": {api.config.token}, + "token": {api.token}, } if item.Timestamp != "" { values.Set("timestamp", string(item.Timestamp)) @@ -86,8 +87,9 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I if item.Comment != "" { values.Set("file_comment", string(item.Comment)) } + response := &SlackResponse{} - if err := post(ctx, "stars.remove", values, response, api.debug); err != nil { + if err := post(ctx, api.httpclient, "stars.remove", values, response, api.debug); err != nil { return err } if !response.Ok { @@ -104,7 +106,7 @@ func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) { // ListStarsContext returns information about the stars a user added with a custom context func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, *Paging, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if params.User != DEFAULT_STARS_USER { values.Add("user", params.User) @@ -115,8 +117,9 @@ func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) if params.Page != DEFAULT_STARS_PAGE { values.Add("page", strconv.Itoa(params.Page)) } + response := &listResponseFull{} - err := post(ctx, "stars.list", values, response, api.debug) + err := post(ctx, api.httpclient, "stars.list", values, response, api.debug) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/team.go b/vendor/github.com/nlopes/slack/team.go index e70ac57..53ba572 100644 --- a/vendor/github.com/nlopes/slack/team.go +++ b/vendor/github.com/nlopes/slack/team.go @@ -67,9 +67,9 @@ func NewAccessLogParameters() AccessLogParameters { } } -func teamRequest(ctx context.Context, path string, values url.Values, debug bool) (*TeamResponse, error) { +func teamRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*TeamResponse, error) { response := &TeamResponse{} - err := post(ctx, path, values, response, debug) + err := post(ctx, client, path, values, response, debug) if err != nil { return nil, err } @@ -81,9 +81,9 @@ func teamRequest(ctx context.Context, path string, values url.Values, debug bool return response, nil } -func billableInfoRequest(ctx context.Context, path string, values url.Values, debug bool) (map[string]BillingActive, error) { +func billableInfoRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (map[string]BillingActive, error) { response := &BillableInfoResponse{} - err := post(ctx, path, values, response, debug) + err := post(ctx, client, path, values, response, debug) if err != nil { return nil, err } @@ -95,9 +95,9 @@ func billableInfoRequest(ctx context.Context, path string, values url.Values, de return response.BillableInfo, nil } -func accessLogsRequest(ctx context.Context, path string, values url.Values, debug bool) (*LoginResponse, error) { +func accessLogsRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*LoginResponse, error) { response := &LoginResponse{} - err := post(ctx, path, values, response, debug) + err := post(ctx, client, path, values, response, debug) if err != nil { return nil, err } @@ -115,10 +115,10 @@ func (api *Client) GetTeamInfo() (*TeamInfo, error) { // GetTeamInfoContext gets the Team Information of the user with a custom context func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } - response, err := teamRequest(ctx, "team.info", values, api.debug) + response, err := teamRequest(ctx, api.httpclient, "team.info", values, api.debug) if err != nil { return nil, err } @@ -133,7 +133,7 @@ func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging, // GetAccessLogsContext retrieves a page of logins according to the parameters given with a custom context func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, *Paging, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if params.Count != DEFAULT_LOGINS_COUNT { values.Add("count", strconv.Itoa(params.Count)) @@ -141,7 +141,8 @@ func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogPar if params.Page != DEFAULT_LOGINS_PAGE { values.Add("page", strconv.Itoa(params.Page)) } - response, err := accessLogsRequest(ctx, "team.accessLogs", values, api.debug) + + response, err := accessLogsRequest(ctx, api.httpclient, "team.accessLogs", values, api.debug) if err != nil { return nil, nil, err } @@ -154,11 +155,11 @@ func (api *Client) GetBillableInfo(user string) (map[string]BillingActive, error func (api *Client) GetBillableInfoContext(ctx context.Context, user string) (map[string]BillingActive, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "user": {user}, } - return billableInfoRequest(ctx, "team.billableInfo", values, api.debug) + return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug) } // GetBillableInfoForTeam returns the billing_active status of all users on the team. @@ -169,8 +170,8 @@ func (api *Client) GetBillableInfoForTeam() (map[string]BillingActive, error) { // GetBillableInfoForTeamContext returns the billing_active status of all users on the team with a custom context func (api *Client) GetBillableInfoForTeamContext(ctx context.Context) (map[string]BillingActive, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } - return billableInfoRequest(ctx, "team.billableInfo", values, api.debug) + return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug) } diff --git a/vendor/github.com/nlopes/slack/usergroups.go b/vendor/github.com/nlopes/slack/usergroups.go index de9f986..4abcf0c 100644 --- a/vendor/github.com/nlopes/slack/usergroups.go +++ b/vendor/github.com/nlopes/slack/usergroups.go @@ -40,9 +40,9 @@ type userGroupResponseFull struct { SlackResponse } -func userGroupRequest(ctx context.Context, path string, values url.Values, debug bool) (*userGroupResponseFull, error) { +func userGroupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userGroupResponseFull, error) { response := &userGroupResponseFull{} - err := post(ctx, path, values, response, debug) + err := post(ctx, client, path, values, response, debug) if err != nil { return nil, err } @@ -60,7 +60,7 @@ func (api *Client) CreateUserGroup(userGroup UserGroup) (UserGroup, error) { // CreateUserGroupContext creates a new user group with a custom context func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "name": {userGroup.Name}, } @@ -76,7 +76,7 @@ func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGro values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")} } - response, err := userGroupRequest(ctx, "usergroups.create", values, api.debug) + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.create", values, api.debug) if err != nil { return UserGroup{}, err } @@ -91,11 +91,11 @@ func (api *Client) DisableUserGroup(userGroup string) (UserGroup, error) { // DisableUserGroupContext disables an existing user group with a custom context func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "usergroup": {userGroup}, } - response, err := userGroupRequest(ctx, "usergroups.disable", values, api.debug) + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.disable", values, api.debug) if err != nil { return UserGroup{}, err } @@ -110,11 +110,11 @@ func (api *Client) EnableUserGroup(userGroup string) (UserGroup, error) { // EnableUserGroupContext enables an existing user group with a custom context func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "usergroup": {userGroup}, } - response, err := userGroupRequest(ctx, "usergroups.enable", values, api.debug) + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.enable", values, api.debug) if err != nil { return UserGroup{}, err } @@ -129,10 +129,10 @@ func (api *Client) GetUserGroups() ([]UserGroup, error) { // GetUserGroupsContext returns a list of user groups for the team with a custom context func (api *Client) GetUserGroupsContext(ctx context.Context) ([]UserGroup, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } - response, err := userGroupRequest(ctx, "usergroups.list", values, api.debug) + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.list", values, api.debug) if err != nil { return nil, err } @@ -147,7 +147,7 @@ func (api *Client) UpdateUserGroup(userGroup UserGroup) (UserGroup, error) { // UpdateUserGroupContext will update an existing user group with a custom context func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "usergroup": {userGroup.ID}, } @@ -163,7 +163,7 @@ func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGro values["description"] = []string{userGroup.Description} } - response, err := userGroupRequest(ctx, "usergroups.update", values, api.debug) + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.update", values, api.debug) if err != nil { return UserGroup{}, err } @@ -178,11 +178,11 @@ func (api *Client) GetUserGroupMembers(userGroup string) ([]string, error) { // GetUserGroupMembersContext will retrieve the current list of users in a group with a custom context func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup string) ([]string, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "usergroup": {userGroup}, } - response, err := userGroupRequest(ctx, "usergroups.users.list", values, api.debug) + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.list", values, api.debug) if err != nil { return []string{}, err } @@ -197,12 +197,12 @@ func (api *Client) UpdateUserGroupMembers(userGroup string, members string) (Use // UpdateUserGroupMembersContext will update the members of an existing user group with a custom context func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup string, members string) (UserGroup, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "usergroup": {userGroup}, "users": {members}, } - response, err := userGroupRequest(ctx, "usergroups.users.update", values, api.debug) + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.update", values, api.debug) if err != nil { return UserGroup{}, err } diff --git a/vendor/github.com/nlopes/slack/users.go b/vendor/github.com/nlopes/slack/users.go index 0aa9557..5b3dddc 100644 --- a/vendor/github.com/nlopes/slack/users.go +++ b/vendor/github.com/nlopes/slack/users.go @@ -15,29 +15,33 @@ const ( // UserProfile contains all the information details of a given user type UserProfile struct { - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - RealName string `json:"real_name"` - RealNameNormalized string `json:"real_name_normalized"` - Email string `json:"email"` - Skype string `json:"skype"` - Phone string `json:"phone"` - Image24 string `json:"image_24"` - Image32 string `json:"image_32"` - Image48 string `json:"image_48"` - Image72 string `json:"image_72"` - Image192 string `json:"image_192"` - ImageOriginal string `json:"image_original"` - Title string `json:"title"` - BotID string `json:"bot_id,omitempty"` - ApiAppID string `json:"api_app_id,omitempty"` - StatusText string `json:"status_text,omitempty"` - StatusEmoji string `json:"status_emoji,omitempty"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + RealName string `json:"real_name"` + RealNameNormalized string `json:"real_name_normalized"` + DisplayName string `json:"display_name"` + DisplayNameNormalized string `json:"display_name_normalized"` + Email string `json:"email"` + Skype string `json:"skype"` + Phone string `json:"phone"` + Image24 string `json:"image_24"` + Image32 string `json:"image_32"` + Image48 string `json:"image_48"` + Image72 string `json:"image_72"` + Image192 string `json:"image_192"` + ImageOriginal string `json:"image_original"` + Title string `json:"title"` + BotID string `json:"bot_id,omitempty"` + ApiAppID string `json:"api_app_id,omitempty"` + StatusText string `json:"status_text,omitempty"` + StatusEmoji string `json:"status_emoji,omitempty"` + Team string `json:"team"` } // 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"` @@ -52,9 +56,12 @@ type User struct { 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"` } // UserPresence contains details about a user online status @@ -121,9 +128,9 @@ func NewUserSetPhotoParams() UserSetPhotoParams { } } -func userRequest(ctx context.Context, path string, values url.Values, debug bool) (*userResponseFull, error) { +func userRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userResponseFull, error) { response := &userResponseFull{} - err := post(ctx, path, values, response, debug) + err := postForm(ctx, client, SLACK_API+path, values, response, debug) if err != nil { return nil, err } @@ -141,10 +148,11 @@ func (api *Client) GetUserPresence(user string) (*UserPresence, error) { // GetUserPresenceContext will retrieve the current presence status of given user with a custom context. func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*UserPresence, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "user": {user}, } - response, err := userRequest(ctx, "users.getPresence", values, api.debug) + + response, err := userRequest(ctx, api.httpclient, "users.getPresence", values, api.debug) if err != nil { return nil, err } @@ -159,10 +167,11 @@ 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.config.token}, + "token": {api.token}, "user": {user}, } - response, err := userRequest(ctx, "users.info", values, api.debug) + + response, err := userRequest(ctx, api.httpclient, "users.info", values, api.debug) if err != nil { return nil, err } @@ -177,30 +186,50 @@ func (api *Client) GetUsers() ([]User, error) { // GetUsersContext returns the list of users (with their detailed information) with a custom context func (api *Client) GetUsersContext(ctx context.Context) ([]User, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "presence": {"1"}, } - response, err := userRequest(ctx, "users.list", values, api.debug) + + response, err := userRequest(ctx, api.httpclient, "users.list", values, api.debug) if err != nil { return nil, err } return response.Members, nil } +// GetUserByEmail will retrieve the complete user information by email +func (api *Client) GetUserByEmail(email string) (*User, error) { + return api.GetUserByEmailContext(context.Background(), email) +} + +// GetUserByEmailContext will retrieve the complete user information by email with a custom context +func (api *Client) GetUserByEmailContext(ctx context.Context, email string) (*User, error) { + values := url.Values{ + "token": {api.token}, + "email": {email}, + } + response, err := userRequest(ctx, api.httpclient, "users.lookupByEmail", values, api.debug) + if err != nil { + return nil, err + } + return &response.User, nil +} + // SetUserAsActive marks the currently authenticated user as active func (api *Client) SetUserAsActive() error { return api.SetUserAsActiveContext(context.Background()) } // SetUserAsActiveContext marks the currently authenticated user as active with a custom context -func (api *Client) SetUserAsActiveContext(ctx context.Context) error { +func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } - _, err := userRequest(ctx, "users.setActive", values, api.debug) - if err != nil { + + if _, err := userRequest(ctx, api.httpclient, "users.setActive", values, api.debug); err != nil { return err } + return nil } @@ -212,10 +241,11 @@ func (api *Client) SetUserPresence(presence string) error { // SetUserPresenceContext changes the currently authenticated user presence with a custom context func (api *Client) SetUserPresenceContext(ctx context.Context, presence string) error { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "presence": {presence}, } - _, err := userRequest(ctx, "users.setPresence", values, api.debug) + + _, err := userRequest(ctx, api.httpclient, "users.setPresence", values, api.debug) if err != nil { return err } @@ -231,10 +261,11 @@ func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) { // GetUserIdentityContext will retrieve user info available per identity scopes with a custom context func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityResponse, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } response := &UserIdentityResponse{} - err := post(ctx, "users.identity", values, response, api.debug) + + err := postForm(ctx, api.httpclient, SLACK_API+"users.identity", values, response, api.debug) if err != nil { return nil, err } @@ -245,15 +276,15 @@ func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityRes } // SetUserPhoto changes the currently authenticated user's profile image -func (api *Client) SetUserPhoto(ctx context.Context, image string, params UserSetPhotoParams) error { - return api.SetUserPhoto(context.Background(), image, params) +func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error { + return api.SetUserPhotoContext(context.Background(), image, params) } // 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 { response := &SlackResponse{} values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if params.CropX != DEFAULT_USER_PHOTO_CROP_X { values.Add("crop_x", string(params.CropX)) @@ -264,7 +295,8 @@ func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params if params.CropW != DEFAULT_USER_PHOTO_CROP_W { values.Add("crop_w", string(params.CropW)) } - err := postLocalWithMultipartResponse(ctx, "users.setPhoto", image, "image", values, response, api.debug) + + err := postLocalWithMultipartResponse(ctx, api.httpclient, SLACK_API+"users.setPhoto", image, "image", values, response, api.debug) if err != nil { return err } @@ -283,9 +315,10 @@ func (api *Client) DeleteUserPhoto() error { func (api *Client) DeleteUserPhotoContext(ctx context.Context) error { response := &SlackResponse{} values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } - err := post(ctx, "users.deletePhoto", values, response, api.debug) + + err := postForm(ctx, api.httpclient, SLACK_API+"users.deletePhoto", values, response, api.debug) if err != nil { return err } @@ -332,13 +365,12 @@ func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, s } values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "profile": {string(profile)}, } response := &userResponseFull{} - - if err = post(ctx, "users.profile.set", values, response, api.debug); err != nil { + if err = postForm(ctx, api.httpclient, SLACK_API+"users.profile.set", values, response, api.debug); err != nil { return err } diff --git a/vendor/github.com/nlopes/slack/websocket.go b/vendor/github.com/nlopes/slack/websocket.go index f3c9cbd..f28c945 100644 --- a/vendor/github.com/nlopes/slack/websocket.go +++ b/vendor/github.com/nlopes/slack/websocket.go @@ -5,7 +5,7 @@ import ( "errors" "time" - "golang.org/x/net/websocket" + "github.com/gorilla/websocket" ) const ( @@ -27,6 +27,7 @@ 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. forcePing chan bool rawEvents chan json.RawMessage wasIntentional bool @@ -59,9 +60,14 @@ type RTMOptions struct { // Disconnect and wait, blocking until a successful disconnection. func (rtm *RTM) Disconnect() error { + // this channel is always closed on disconnect. lets the ManagedConnection() function + // properly clean up. + close(rtm.disconnected) + if !rtm.isConnected { return errors.New("Invalid call to Disconnect - Slack API is already disconnected") } + rtm.killChannel <- true return nil } diff --git a/vendor/github.com/nlopes/slack/websocket_internals.go b/vendor/github.com/nlopes/slack/websocket_internals.go index 2a8abe6..e8374b0 100644 --- a/vendor/github.com/nlopes/slack/websocket_internals.go +++ b/vendor/github.com/nlopes/slack/websocket_internals.go @@ -63,6 +63,13 @@ func (m *MessageTooLongEvent) Error() string { return fmt.Sprintf("Message too long (max %d characters)", m.MaxLength) } +// RateLimitEvent is used when Slack warns that rate-limits are being hit. +type RateLimitEvent struct{} + +func (e *RateLimitEvent) Error() string { + return "Messages are being sent too fast." +} + // OutgoingErrorEvent contains information in case there were errors sending messages type OutgoingErrorEvent struct { Message OutgoingMessage diff --git a/vendor/github.com/nlopes/slack/websocket_managed_conn.go b/vendor/github.com/nlopes/slack/websocket_managed_conn.go index 9c48852..7f7f353 100644 --- a/vendor/github.com/nlopes/slack/websocket_managed_conn.go +++ b/vendor/github.com/nlopes/slack/websocket_managed_conn.go @@ -4,10 +4,11 @@ import ( "encoding/json" "fmt" "io" + "net/http" "reflect" "time" - "golang.org/x/net/websocket" + "github.com/gorilla/websocket" ) // ManageConnection can be called on a Slack RTM instance returned by the @@ -33,6 +34,7 @@ func (rtm *RTM) ManageConnection() { // if err != nil then the connection is sucessful - otherwise it is // fatal if err != nil { + rtm.Debugf("Failed to connect with RTM on try %d: %s", connectionCount, err) return } rtm.info = info @@ -44,6 +46,8 @@ func (rtm *RTM) ManageConnection() { rtm.conn = conn rtm.isConnected = true + 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 @@ -89,6 +93,7 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke } // check for fatal errors - currently only invalid_auth if sErr, ok := err.(*WebError); ok && (sErr.Error() == "invalid_auth" || sErr.Error() == "account_inactive") { + rtm.Debugf("Invalid auth when connecting with RTM: %s", err) rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}} return nil, nil, sErr } @@ -99,6 +104,15 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke Attempt: boff.attempts, ErrorObj: err, }} + + // check if Disconnect() has been invoked. + select { + case _ = <-rtm.disconnected: + rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: true}} + return nil, nil, fmt.Errorf("disconnect received while trying to connect") + default: + } + // get time we should wait before attempting to connect again dur := boff.Duration() rtm.Debugf("reconnection %d failed: %s", boff.attempts+1, err) @@ -116,16 +130,24 @@ func (rtm *RTM) startRTMAndDial(useRTMStart bool) (*Info, *websocket.Conn, error var err error if useRTMStart { + rtm.Debugf("Starting RTM") info, url, err = rtm.StartRTM() } else { + rtm.Debugf("Connecting to RTM") info, url, err = rtm.ConnectRTM() } if err != nil { + rtm.Debugf("Failed to start or connect to RTM: %s", err) return nil, nil, err } - conn, err := websocketProxyDial(url, "http://api.slack.com") + rtm.Debugf("Dialing to websocket on url %s", url) + // Only use HTTPS for connections to prevent MITM attacks on the connection. + upgradeHeader := http.Header{} + upgradeHeader.Add("Origin", "https://api.slack.com") + conn, _, err := websocket.DefaultDialer.Dial(url, upgradeHeader) if err != nil { + rtm.Debugf("Failed to dial to the websocket: %s", err) return nil, nil, err } return info, conn, err @@ -208,7 +230,7 @@ func (rtm *RTM) sendWithDeadline(msg interface{}) error { if err := rtm.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { return err } - if err := websocket.JSON.Send(rtm.conn, msg); err != nil { + if err := rtm.conn.WriteJSON(msg); err != nil { return err } // remove write deadline @@ -263,7 +285,7 @@ func (rtm *RTM) ping() error { // This will block until a frame is available from the websocket. func (rtm *RTM) receiveIncomingEvent() { event := json.RawMessage{} - err := websocket.JSON.Receive(rtm.conn, &event) + err := rtm.conn.ReadJSON(&event) if err == io.EOF { // EOF's don't seem to signify a failed connection so instead we ignore // them here and detect a failed connection upon attempting to send a @@ -317,10 +339,19 @@ func (rtm *RTM) handleAck(event json.RawMessage) { rtm.Debugln(" -> Erroneous 'ack' event:", string(event)) return } + if ack.Ok { rtm.IncomingEvents <- RTMEvent{"ack", ack} + } else if ack.RTMResponse.Error != nil { + // As there is no documentation for RTM error-codes, this + // identification of a rate-limit warning is very brittle. + if ack.RTMResponse.Error.Code == -1 && ack.RTMResponse.Error.Msg == "slow down, too many messages..." { + rtm.IncomingEvents <- RTMEvent{"ack_error", &RateLimitEvent{}} + } else { + rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ack.Error}} + } } else { - rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ack.Error}} + rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{fmt.Errorf("ack decode failure")}} } } diff --git a/vendor/github.com/nlopes/slack/websocket_proxy.go b/vendor/github.com/nlopes/slack/websocket_proxy.go deleted file mode 100644 index 440015d..0000000 --- a/vendor/github.com/nlopes/slack/websocket_proxy.go +++ /dev/null @@ -1,83 +0,0 @@ -package slack - -import ( - "crypto/tls" - "errors" - "net" - "net/http" - "net/http/httputil" - "net/url" - "os" - "strings" - - "golang.org/x/net/websocket" -) - -// Taken and reworked from: https://gist.github.com/madmo/8548738 -func websocketHTTPConnect(proxy, urlString string) (net.Conn, error) { - p, err := net.Dial("tcp", proxy) - if err != nil { - return nil, err - } - - turl, err := url.Parse(urlString) - if err != nil { - return nil, err - } - - req := http.Request{ - Method: "CONNECT", - URL: &url.URL{}, - Host: turl.Host, - } - - cc := httputil.NewProxyClientConn(p, nil) - cc.Do(&req) - if err != nil && err != httputil.ErrPersistEOF { - return nil, err - } - - rwc, _ := cc.Hijack() - - return rwc, nil -} - -func websocketProxyDial(urlString, origin string) (ws *websocket.Conn, err error) { - if os.Getenv("HTTP_PROXY") == "" { - return websocket.Dial(urlString, "", origin) - } - - purl, err := url.Parse(os.Getenv("HTTP_PROXY")) - if err != nil { - return nil, err - } - - config, err := websocket.NewConfig(urlString, origin) - if err != nil { - return nil, err - } - - client, err := websocketHTTPConnect(purl.Host, urlString) - if err != nil { - return nil, err - } - - switch config.Location.Scheme { - case "ws": - case "wss": - tlsClient := tls.Client(client, &tls.Config{ - ServerName: strings.Split(config.Location.Host, ":")[0], - }) - err := tlsClient.Handshake() - if err != nil { - tlsClient.Close() - return nil, err - } - client = tlsClient - - default: - return nil, errors.New("invalid websocket schema") - } - - return websocket.NewClient(config, client) -} diff --git a/vendor/github.com/nlopes/slack/websocket_utils.go b/vendor/github.com/nlopes/slack/websocket_utils.go deleted file mode 100644 index b3d0ec8..0000000 --- a/vendor/github.com/nlopes/slack/websocket_utils.go +++ /dev/null @@ -1,20 +0,0 @@ -package slack - -import ( - "net" - "net/url" -) - -var portMapping = map[string]string{"ws": "80", "wss": "443"} - -func websocketizeURLPort(orig string) (string, error) { - urlObj, err := url.ParseRequestURI(orig) - if err != nil { - return "", err - } - _, _, err = net.SplitHostPort(urlObj.Host) - if err != nil { - return urlObj.Scheme + "://" + urlObj.Host + ":" + portMapping[urlObj.Scheme] + urlObj.Path, nil - } - return orig, nil -} diff --git a/vendor/github.com/nsf/termbox-go/README.md b/vendor/github.com/nsf/termbox-go/README.md index f46005e..fcc493d 100644 --- a/vendor/github.com/nsf/termbox-go/README.md +++ b/vendor/github.com/nsf/termbox-go/README.md @@ -1,3 +1,5 @@ +[![GoDoc](https://godoc.org/github.com/nsf/termbox-go?status.svg)](http://godoc.org/github.com/nsf/termbox-go) + ## Termbox Termbox is a library that provides a minimalistic API which allows the programmer to write text-based user interfaces. The library is crossplatform and has both terminal-based implementations on *nix operating systems and a winapi console based implementation for windows operating systems. The basic idea is an abstraction of the greatest common subset of features available on all major terminals and other terminal-like APIs in a minimalistic fashion. Small API means it is easy to implement, test, maintain and learn it, that's what makes the termbox a distinct library in its area. @@ -33,6 +35,4 @@ There are also some interesting projects using termbox-go: - [jv](https://github.com/maxzender/jv) helps you view JSON on the command-line. - [pinger](https://github.com/hirose31/pinger) helps you to monitor numerous hosts using ICMP ECHO_REQUEST. - [vixl44](https://github.com/sebashwa/vixl44) lets you create pixel art inside your terminal using vim movements - -### API reference -[godoc.org/github.com/nsf/termbox-go](http://godoc.org/github.com/nsf/termbox-go) + - [zterm](https://github.com/varunrau/zterm) is a typing game inspired by http://zty.pe/ diff --git a/vendor/github.com/nsf/termbox-go/api.go b/vendor/github.com/nsf/termbox-go/api.go index 94cd9b6..d530ab5 100644 --- a/vendor/github.com/nsf/termbox-go/api.go +++ b/vendor/github.com/nsf/termbox-go/api.go @@ -8,6 +8,7 @@ import "os" import "os/signal" import "syscall" import "runtime" +import "time" // public API @@ -253,8 +254,8 @@ func CellBuffer() []Cell { // NOTE: This API is experimental and may change in future. func ParseEvent(data []byte) Event { event := Event{Type: EventKey} - ok := extract_event(data, &event) - if !ok { + status := extract_event(data, &event, false) + if status != event_extracted { return Event{Type: EventNone, N: event.N} } return event @@ -303,34 +304,65 @@ func PollRawEvent(data []byte) Event { // Wait for an event and return it. This is a blocking function call. func PollEvent() Event { + // Constant governing macOS specific behavior. See https://github.com/nsf/termbox-go/issues/132 + // This is an arbitrary delay which hopefully will be enough time for any lagging + // partial escape sequences to come through. + const esc_wait_delay = 100 * time.Millisecond + var event Event + var esc_wait_timer *time.Timer + var esc_timeout <-chan time.Time // try to extract event from input buffer, return on success event.Type = EventKey - ok := extract_event(inbuf, &event) + status := extract_event(inbuf, &event, true) if event.N != 0 { copy(inbuf, inbuf[event.N:]) inbuf = inbuf[:len(inbuf)-event.N] } - if ok { + if status == event_extracted { return event + } else if status == esc_wait { + esc_wait_timer = time.NewTimer(esc_wait_delay) + esc_timeout = esc_wait_timer.C } for { select { case ev := <-input_comm: + if esc_wait_timer != nil { + if !esc_wait_timer.Stop() { + <-esc_wait_timer.C + } + esc_wait_timer = nil + } + if ev.err != nil { return Event{Type: EventError, Err: ev.err} } inbuf = append(inbuf, ev.data...) input_comm <- ev - ok := extract_event(inbuf, &event) + status := extract_event(inbuf, &event, true) if event.N != 0 { copy(inbuf, inbuf[event.N:]) inbuf = inbuf[:len(inbuf)-event.N] } - if ok { + if status == event_extracted { + return event + } else if status == esc_wait { + esc_wait_timer = time.NewTimer(esc_wait_delay) + esc_timeout = esc_wait_timer.C + } + case <-esc_timeout: + esc_wait_timer = nil + + status := extract_event(inbuf, &event, false) + if event.N != 0 { + copy(inbuf, inbuf[event.N:]) + inbuf = inbuf[:len(inbuf)-event.N] + } + if status == event_extracted { return event } case <-interrupt_comm: diff --git a/vendor/github.com/nsf/termbox-go/api_common.go b/vendor/github.com/nsf/termbox-go/api_common.go index 9f23661..5ca1371 100644 --- a/vendor/github.com/nsf/termbox-go/api_common.go +++ b/vendor/github.com/nsf/termbox-go/api_common.go @@ -148,7 +148,7 @@ const ( // using bitwise OR ('|'). Although, colors cannot be combined. But you can // combine attributes and a single color. // -// It's worth mentioning that some platforms don't support certain attibutes. +// It's worth mentioning that some platforms don't support certain attributes. // For example windows console doesn't support AttrUnderline. And on some // terminals applying AttrBold to background may result in blinking text. Use // them with caution and test your code on various terminals. diff --git a/vendor/github.com/nsf/termbox-go/escwait.go b/vendor/github.com/nsf/termbox-go/escwait.go new file mode 100644 index 0000000..b7bbb89 --- /dev/null +++ b/vendor/github.com/nsf/termbox-go/escwait.go @@ -0,0 +1,11 @@ +// +build !darwin + +package termbox + +// On all systems other than macOS, disable behavior which will wait before +// deciding that the escape key was pressed, to account for partially send +// escape sequences, especially with regard to lengthy mouse sequences. +// See https://github.com/nsf/termbox-go/issues/132 +func enable_wait_for_escape_sequence() bool { + return false +} diff --git a/vendor/github.com/nsf/termbox-go/escwait_darwin.go b/vendor/github.com/nsf/termbox-go/escwait_darwin.go new file mode 100644 index 0000000..dde69b6 --- /dev/null +++ b/vendor/github.com/nsf/termbox-go/escwait_darwin.go @@ -0,0 +1,9 @@ +package termbox + +// On macOS, enable behavior which will wait before deciding that the escape +// key was pressed, to account for partially send escape sequences, especially +// with regard to lengthy mouse sequences. +// See https://github.com/nsf/termbox-go/issues/132 +func enable_wait_for_escape_sequence() bool { + return true +} diff --git a/vendor/github.com/nsf/termbox-go/syscalls.go b/vendor/github.com/nsf/termbox-go/syscalls.go new file mode 100644 index 0000000..4f52bb9 --- /dev/null +++ b/vendor/github.com/nsf/termbox-go/syscalls.go @@ -0,0 +1,39 @@ +// +build ignore + +package termbox + +/* +#include +#include +*/ +import "C" + +type syscall_Termios C.struct_termios + +const ( + syscall_IGNBRK = C.IGNBRK + syscall_BRKINT = C.BRKINT + syscall_PARMRK = C.PARMRK + syscall_ISTRIP = C.ISTRIP + syscall_INLCR = C.INLCR + syscall_IGNCR = C.IGNCR + syscall_ICRNL = C.ICRNL + syscall_IXON = C.IXON + syscall_OPOST = C.OPOST + syscall_ECHO = C.ECHO + syscall_ECHONL = C.ECHONL + syscall_ICANON = C.ICANON + syscall_ISIG = C.ISIG + syscall_IEXTEN = C.IEXTEN + syscall_CSIZE = C.CSIZE + syscall_PARENB = C.PARENB + syscall_CS8 = C.CS8 + syscall_VMIN = C.VMIN + syscall_VTIME = C.VTIME + + // on darwin change these to (on *bsd too?): + // C.TIOCGETA + // C.TIOCSETA + syscall_TCGETS = C.TCGETS + syscall_TCSETS = C.TCSETS +) diff --git a/vendor/github.com/nsf/termbox-go/termbox.go b/vendor/github.com/nsf/termbox-go/termbox.go index c2d86c6..fbe4c3d 100644 --- a/vendor/github.com/nsf/termbox-go/termbox.go +++ b/vendor/github.com/nsf/termbox-go/termbox.go @@ -41,6 +41,14 @@ type input_event struct { err error } +type extract_event_res int + +const ( + event_not_extracted extract_event_res = iota + event_extracted + esc_wait +) + var ( // term specific sequences keys []string @@ -417,7 +425,7 @@ func parse_escape_sequence(event *Event, buf []byte) (int, bool) { } } - // if none of the keys match, let's try mouse seqences + // if none of the keys match, let's try mouse sequences return parse_mouse_event(event, bufstr) } @@ -440,17 +448,27 @@ func extract_raw_event(data []byte, event *Event) bool { return true } -func extract_event(inbuf []byte, event *Event) bool { +func extract_event(inbuf []byte, event *Event, allow_esc_wait bool) extract_event_res { if len(inbuf) == 0 { event.N = 0 - return false + return event_not_extracted } if inbuf[0] == '\033' { // possible escape sequence if n, ok := parse_escape_sequence(event, inbuf); n != 0 { event.N = n - return ok + if ok { + return event_extracted + } else { + return event_not_extracted + } + } + + // possible partially read escape sequence; trigger a wait if appropriate + if enable_wait_for_escape_sequence() && allow_esc_wait { + event.N = 0 + return esc_wait } // it's not escape sequence, then it's Alt or Esc, check input_mode @@ -461,17 +479,17 @@ func extract_event(inbuf []byte, event *Event) bool { event.Key = KeyEsc event.Mod = 0 event.N = 1 - return true + return event_extracted case input_mode&InputAlt != 0: // if we're in alt mode, set Alt modifier to event and redo parsing event.Mod = ModAlt - ok := extract_event(inbuf[1:], event) - if ok { + status := extract_event(inbuf[1:], event, false) + if status == event_extracted { event.N++ } else { event.N = 0 } - return ok + return status default: panic("unreachable") } @@ -486,7 +504,7 @@ func extract_event(inbuf []byte, event *Event) bool { event.Ch = 0 event.Key = Key(inbuf[0]) event.N = 1 - return true + return event_extracted } // the only possible option is utf8 rune @@ -494,10 +512,10 @@ func extract_event(inbuf []byte, event *Event) bool { event.Ch = r event.Key = 0 event.N = n - return true + return event_extracted } - return false + return event_not_extracted } func fcntl(fd int, cmd int, arg int) (val int, err error) { diff --git a/vendor/github.com/nsf/termbox-go/terminfo.go b/vendor/github.com/nsf/termbox-go/terminfo.go index 35dbd70..5d38fce 100644 --- a/vendor/github.com/nsf/termbox-go/terminfo.go +++ b/vendor/github.com/nsf/termbox-go/terminfo.go @@ -151,11 +151,16 @@ func setup_term() (err error) { return } + number_sec_len := int16(2) + if header[0] == 542 { // doc says it should be octal 0542, but what I see it terminfo files is 542, learn to program please... thank you.. + number_sec_len = 4 + } + if (header[1]+header[2])%2 != 0 { // old quirk to align everything on word boundaries header[2] += 1 } - str_offset = ti_header_length + header[1] + header[2] + 2*header[3] + str_offset = ti_header_length + header[1] + header[2] + number_sec_len*header[3] table_offset = str_offset + 2*header[4] keys = make([]string, 0xFFFF-key_min) diff --git a/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go b/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go index 63277d5..6850e6c 100644 --- a/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go +++ b/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go @@ -122,8 +122,8 @@ Outer: // RankFind is similar to Find, except it will also rank all matches using // Levenshtein distance. -func RankFind(source string, targets []string) Ranks { - var r Ranks +func RankFind(source string, targets []string) ranks { + var r ranks for _, target := range find(source, targets, noop) { distance := LevenshteinDistance(source, target) r = append(r, Rank{source, target, distance}) @@ -132,8 +132,8 @@ func RankFind(source string, targets []string) Ranks { } // RankFindFold is a case-insensitive version of RankFind. -func RankFindFold(source string, targets []string) Ranks { - var r Ranks +func RankFindFold(source string, targets []string) ranks { + var r ranks for _, target := range find(source, targets, unicode.ToLower) { distance := LevenshteinDistance(source, target) r = append(r, Rank{source, target, distance}) @@ -152,16 +152,16 @@ type Rank struct { Distance int } -type Ranks []Rank +type ranks []Rank -func (r Ranks) Len() int { +func (r ranks) Len() int { return len(r) } -func (r Ranks) Swap(i, j int) { +func (r ranks) Swap(i, j int) { r[i], r[j] = r[j], r[i] } -func (r Ranks) Less(i, j int) bool { +func (r ranks) Less(i, j int) bool { return r[i].Distance < r[j].Distance } diff --git a/vendor/golang.org/x/net/LICENSE b/vendor/golang.org/x/net/LICENSE deleted file mode 100644 index 6a66aea..0000000 --- a/vendor/golang.org/x/net/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/net/PATENTS b/vendor/golang.org/x/net/PATENTS deleted file mode 100644 index 7330990..0000000 --- a/vendor/golang.org/x/net/PATENTS +++ /dev/null @@ -1,22 +0,0 @@ -Additional IP Rights Grant (Patents) - -"This implementation" means the copyrightable works distributed by -Google as part of the Go project. - -Google hereby grants to You a perpetual, worldwide, non-exclusive, -no-charge, royalty-free, irrevocable (except as stated in this section) -patent license to make, have made, use, offer to sell, sell, import, -transfer and otherwise run, modify and propagate the contents of this -implementation of Go, where such license applies only to those patent -claims, both currently owned or controlled by Google and acquired in -the future, licensable by Google that are necessarily infringed by this -implementation of Go. This grant does not include claims that would be -infringed only as a consequence of further modification of this -implementation. If you or your agent or exclusive licensee institute or -order or agree to the institution of patent litigation against any -entity (including a cross-claim or counterclaim in a lawsuit) alleging -that this implementation of Go or any code incorporated within this -implementation of Go constitutes direct or contributory patent -infringement, or inducement of patent infringement, then any patent -rights granted to you under this License for this implementation of Go -shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/net/websocket/client.go b/vendor/golang.org/x/net/websocket/client.go deleted file mode 100644 index 69a4ac7..0000000 --- a/vendor/golang.org/x/net/websocket/client.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2009 The Go 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 ( - "bufio" - "io" - "net" - "net/http" - "net/url" -) - -// DialError is an error that occurs while dialling a websocket server. -type DialError struct { - *Config - Err error -} - -func (e *DialError) Error() string { - return "websocket.Dial " + e.Config.Location.String() + ": " + e.Err.Error() -} - -// NewConfig creates a new WebSocket config for client connection. -func NewConfig(server, origin string) (config *Config, err error) { - config = new(Config) - config.Version = ProtocolVersionHybi13 - config.Location, err = url.ParseRequestURI(server) - if err != nil { - return - } - config.Origin, err = url.ParseRequestURI(origin) - if err != nil { - return - } - config.Header = http.Header(make(map[string][]string)) - return -} - -// NewClient creates a new WebSocket client connection over rwc. -func NewClient(config *Config, rwc io.ReadWriteCloser) (ws *Conn, err error) { - br := bufio.NewReader(rwc) - bw := bufio.NewWriter(rwc) - err = hybiClientHandshake(config, br, bw) - if err != nil { - return - } - buf := bufio.NewReadWriter(br, bw) - ws = newHybiClientConn(config, buf, rwc) - return -} - -// Dial opens a new client connection to a WebSocket. -func Dial(url_, protocol, origin string) (ws *Conn, err error) { - config, err := NewConfig(url_, origin) - if err != nil { - return nil, err - } - if protocol != "" { - config.Protocol = []string{protocol} - } - return DialConfig(config) -} - -var portMap = map[string]string{ - "ws": "80", - "wss": "443", -} - -func parseAuthority(location *url.URL) string { - if _, ok := portMap[location.Scheme]; ok { - if _, _, err := net.SplitHostPort(location.Host); err != nil { - return net.JoinHostPort(location.Host, portMap[location.Scheme]) - } - } - return location.Host -} - -// DialConfig opens a new client connection to a WebSocket with a config. -func DialConfig(config *Config) (ws *Conn, err error) { - var client net.Conn - if config.Location == nil { - return nil, &DialError{config, ErrBadWebSocketLocation} - } - if config.Origin == nil { - return nil, &DialError{config, ErrBadWebSocketOrigin} - } - dialer := config.Dialer - if dialer == nil { - dialer = &net.Dialer{} - } - client, err = dialWithDialer(dialer, config) - if err != nil { - goto Error - } - ws, err = NewClient(config, client) - if err != nil { - client.Close() - goto Error - } - return - -Error: - return nil, &DialError{config, err} -} diff --git a/vendor/golang.org/x/net/websocket/dial.go b/vendor/golang.org/x/net/websocket/dial.go deleted file mode 100644 index 2dab943..0000000 --- a/vendor/golang.org/x/net/websocket/dial.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2015 The Go 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 ( - "crypto/tls" - "net" -) - -func dialWithDialer(dialer *net.Dialer, config *Config) (conn net.Conn, err error) { - switch config.Location.Scheme { - case "ws": - conn, err = dialer.Dial("tcp", parseAuthority(config.Location)) - - case "wss": - conn, err = tls.DialWithDialer(dialer, "tcp", parseAuthority(config.Location), config.TlsConfig) - - default: - err = ErrBadScheme - } - return -} diff --git a/vendor/golang.org/x/net/websocket/hybi.go b/vendor/golang.org/x/net/websocket/hybi.go deleted file mode 100644 index 8cffdd1..0000000 --- a/vendor/golang.org/x/net/websocket/hybi.go +++ /dev/null @@ -1,583 +0,0 @@ -// Copyright 2011 The Go 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 - -// This file implements a protocol of hybi draft. -// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 - -import ( - "bufio" - "bytes" - "crypto/rand" - "crypto/sha1" - "encoding/base64" - "encoding/binary" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - "strings" -) - -const ( - websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - - closeStatusNormal = 1000 - closeStatusGoingAway = 1001 - closeStatusProtocolError = 1002 - closeStatusUnsupportedData = 1003 - closeStatusFrameTooLarge = 1004 - closeStatusNoStatusRcvd = 1005 - closeStatusAbnormalClosure = 1006 - closeStatusBadMessageData = 1007 - closeStatusPolicyViolation = 1008 - closeStatusTooBigData = 1009 - closeStatusExtensionMismatch = 1010 - - maxControlFramePayloadLength = 125 -) - -var ( - ErrBadMaskingKey = &ProtocolError{"bad masking key"} - ErrBadPongMessage = &ProtocolError{"bad pong message"} - ErrBadClosingStatus = &ProtocolError{"bad closing status"} - ErrUnsupportedExtensions = &ProtocolError{"unsupported extensions"} - ErrNotImplemented = &ProtocolError{"not implemented"} - - handshakeHeader = map[string]bool{ - "Host": true, - "Upgrade": true, - "Connection": true, - "Sec-Websocket-Key": true, - "Sec-Websocket-Origin": true, - "Sec-Websocket-Version": true, - "Sec-Websocket-Protocol": true, - "Sec-Websocket-Accept": true, - } -) - -// A hybiFrameHeader is a frame header as defined in hybi draft. -type hybiFrameHeader struct { - Fin bool - Rsv [3]bool - OpCode byte - Length int64 - MaskingKey []byte - - data *bytes.Buffer -} - -// A hybiFrameReader is a reader for hybi frame. -type hybiFrameReader struct { - reader io.Reader - - header hybiFrameHeader - pos int64 - length int -} - -func (frame *hybiFrameReader) Read(msg []byte) (n int, err error) { - n, err = frame.reader.Read(msg) - if frame.header.MaskingKey != nil { - for i := 0; i < n; i++ { - msg[i] = msg[i] ^ frame.header.MaskingKey[frame.pos%4] - frame.pos++ - } - } - return n, err -} - -func (frame *hybiFrameReader) PayloadType() byte { return frame.header.OpCode } - -func (frame *hybiFrameReader) HeaderReader() io.Reader { - if frame.header.data == nil { - return nil - } - if frame.header.data.Len() == 0 { - return nil - } - return frame.header.data -} - -func (frame *hybiFrameReader) TrailerReader() io.Reader { return nil } - -func (frame *hybiFrameReader) Len() (n int) { return frame.length } - -// A hybiFrameReaderFactory creates new frame reader based on its frame type. -type hybiFrameReaderFactory struct { - *bufio.Reader -} - -// NewFrameReader reads a frame header from the connection, and creates new reader for the frame. -// See Section 5.2 Base Framing protocol for detail. -// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17#section-5.2 -func (buf hybiFrameReaderFactory) NewFrameReader() (frame frameReader, err error) { - hybiFrame := new(hybiFrameReader) - frame = hybiFrame - var header []byte - var b byte - // First byte. FIN/RSV1/RSV2/RSV3/OpCode(4bits) - b, err = buf.ReadByte() - if err != nil { - return - } - header = append(header, b) - hybiFrame.header.Fin = ((header[0] >> 7) & 1) != 0 - for i := 0; i < 3; i++ { - j := uint(6 - i) - hybiFrame.header.Rsv[i] = ((header[0] >> j) & 1) != 0 - } - hybiFrame.header.OpCode = header[0] & 0x0f - - // Second byte. Mask/Payload len(7bits) - b, err = buf.ReadByte() - if err != nil { - return - } - header = append(header, b) - mask := (b & 0x80) != 0 - b &= 0x7f - lengthFields := 0 - switch { - case b <= 125: // Payload length 7bits. - hybiFrame.header.Length = int64(b) - case b == 126: // Payload length 7+16bits - lengthFields = 2 - case b == 127: // Payload length 7+64bits - lengthFields = 8 - } - for i := 0; i < lengthFields; i++ { - b, err = buf.ReadByte() - if err != nil { - return - } - if lengthFields == 8 && i == 0 { // MSB must be zero when 7+64 bits - b &= 0x7f - } - header = append(header, b) - hybiFrame.header.Length = hybiFrame.header.Length*256 + int64(b) - } - if mask { - // Masking key. 4 bytes. - for i := 0; i < 4; i++ { - b, err = buf.ReadByte() - if err != nil { - return - } - header = append(header, b) - hybiFrame.header.MaskingKey = append(hybiFrame.header.MaskingKey, b) - } - } - hybiFrame.reader = io.LimitReader(buf.Reader, hybiFrame.header.Length) - hybiFrame.header.data = bytes.NewBuffer(header) - hybiFrame.length = len(header) + int(hybiFrame.header.Length) - return -} - -// A HybiFrameWriter is a writer for hybi frame. -type hybiFrameWriter struct { - writer *bufio.Writer - - header *hybiFrameHeader -} - -func (frame *hybiFrameWriter) Write(msg []byte) (n int, err error) { - var header []byte - var b byte - if frame.header.Fin { - b |= 0x80 - } - for i := 0; i < 3; i++ { - if frame.header.Rsv[i] { - j := uint(6 - i) - b |= 1 << j - } - } - b |= frame.header.OpCode - header = append(header, b) - if frame.header.MaskingKey != nil { - b = 0x80 - } else { - b = 0 - } - lengthFields := 0 - length := len(msg) - switch { - case length <= 125: - b |= byte(length) - case length < 65536: - b |= 126 - lengthFields = 2 - default: - b |= 127 - lengthFields = 8 - } - header = append(header, b) - for i := 0; i < lengthFields; i++ { - j := uint((lengthFields - i - 1) * 8) - b = byte((length >> j) & 0xff) - header = append(header, b) - } - if frame.header.MaskingKey != nil { - if len(frame.header.MaskingKey) != 4 { - return 0, ErrBadMaskingKey - } - header = append(header, frame.header.MaskingKey...) - frame.writer.Write(header) - data := make([]byte, length) - for i := range data { - data[i] = msg[i] ^ frame.header.MaskingKey[i%4] - } - frame.writer.Write(data) - err = frame.writer.Flush() - return length, err - } - frame.writer.Write(header) - frame.writer.Write(msg) - err = frame.writer.Flush() - return length, err -} - -func (frame *hybiFrameWriter) Close() error { return nil } - -type hybiFrameWriterFactory struct { - *bufio.Writer - needMaskingKey bool -} - -func (buf hybiFrameWriterFactory) NewFrameWriter(payloadType byte) (frame frameWriter, err error) { - frameHeader := &hybiFrameHeader{Fin: true, OpCode: payloadType} - if buf.needMaskingKey { - frameHeader.MaskingKey, err = generateMaskingKey() - if err != nil { - return nil, err - } - } - return &hybiFrameWriter{writer: buf.Writer, header: frameHeader}, nil -} - -type hybiFrameHandler struct { - conn *Conn - payloadType byte -} - -func (handler *hybiFrameHandler) HandleFrame(frame frameReader) (frameReader, error) { - if handler.conn.IsServerConn() { - // The client MUST mask all frames sent to the server. - if frame.(*hybiFrameReader).header.MaskingKey == nil { - handler.WriteClose(closeStatusProtocolError) - return nil, io.EOF - } - } else { - // The server MUST NOT mask all frames. - if frame.(*hybiFrameReader).header.MaskingKey != nil { - handler.WriteClose(closeStatusProtocolError) - return nil, io.EOF - } - } - if header := frame.HeaderReader(); header != nil { - io.Copy(ioutil.Discard, header) - } - switch frame.PayloadType() { - case ContinuationFrame: - frame.(*hybiFrameReader).header.OpCode = handler.payloadType - case TextFrame, BinaryFrame: - handler.payloadType = frame.PayloadType() - case CloseFrame: - return nil, io.EOF - case PingFrame, PongFrame: - b := make([]byte, maxControlFramePayloadLength) - n, err := io.ReadFull(frame, b) - if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { - return nil, err - } - io.Copy(ioutil.Discard, frame) - if frame.PayloadType() == PingFrame { - if _, err := handler.WritePong(b[:n]); err != nil { - return nil, err - } - } - return nil, nil - } - return frame, nil -} - -func (handler *hybiFrameHandler) WriteClose(status int) (err error) { - handler.conn.wio.Lock() - defer handler.conn.wio.Unlock() - w, err := handler.conn.frameWriterFactory.NewFrameWriter(CloseFrame) - if err != nil { - return err - } - msg := make([]byte, 2) - binary.BigEndian.PutUint16(msg, uint16(status)) - _, err = w.Write(msg) - w.Close() - return err -} - -func (handler *hybiFrameHandler) WritePong(msg []byte) (n int, err error) { - handler.conn.wio.Lock() - defer handler.conn.wio.Unlock() - w, err := handler.conn.frameWriterFactory.NewFrameWriter(PongFrame) - if err != nil { - return 0, err - } - n, err = w.Write(msg) - w.Close() - return n, err -} - -// newHybiConn creates a new WebSocket connection speaking hybi draft protocol. -func newHybiConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { - if buf == nil { - br := bufio.NewReader(rwc) - bw := bufio.NewWriter(rwc) - buf = bufio.NewReadWriter(br, bw) - } - ws := &Conn{config: config, request: request, buf: buf, rwc: rwc, - frameReaderFactory: hybiFrameReaderFactory{buf.Reader}, - frameWriterFactory: hybiFrameWriterFactory{ - buf.Writer, request == nil}, - PayloadType: TextFrame, - defaultCloseStatus: closeStatusNormal} - ws.frameHandler = &hybiFrameHandler{conn: ws} - return ws -} - -// generateMaskingKey generates a masking key for a frame. -func generateMaskingKey() (maskingKey []byte, err error) { - maskingKey = make([]byte, 4) - if _, err = io.ReadFull(rand.Reader, maskingKey); err != nil { - return - } - return -} - -// generateNonce generates a nonce consisting of a randomly selected 16-byte -// value that has been base64-encoded. -func generateNonce() (nonce []byte) { - key := make([]byte, 16) - if _, err := io.ReadFull(rand.Reader, key); err != nil { - panic(err) - } - nonce = make([]byte, 24) - base64.StdEncoding.Encode(nonce, key) - return -} - -// removeZone removes IPv6 zone identifer from host. -// E.g., "[fe80::1%en0]:8080" to "[fe80::1]:8080" -func removeZone(host string) string { - if !strings.HasPrefix(host, "[") { - return host - } - i := strings.LastIndex(host, "]") - if i < 0 { - return host - } - j := strings.LastIndex(host[:i], "%") - if j < 0 { - return host - } - return host[:j] + host[i:] -} - -// getNonceAccept computes the base64-encoded SHA-1 of the concatenation of -// the nonce ("Sec-WebSocket-Key" value) with the websocket GUID string. -func getNonceAccept(nonce []byte) (expected []byte, err error) { - h := sha1.New() - if _, err = h.Write(nonce); err != nil { - return - } - if _, err = h.Write([]byte(websocketGUID)); err != nil { - return - } - expected = make([]byte, 28) - base64.StdEncoding.Encode(expected, h.Sum(nil)) - return -} - -// Client handshake described in draft-ietf-hybi-thewebsocket-protocol-17 -func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) { - bw.WriteString("GET " + config.Location.RequestURI() + " HTTP/1.1\r\n") - - // According to RFC 6874, an HTTP client, proxy, or other - // intermediary must remove any IPv6 zone identifier attached - // to an outgoing URI. - bw.WriteString("Host: " + removeZone(config.Location.Host) + "\r\n") - bw.WriteString("Upgrade: websocket\r\n") - bw.WriteString("Connection: Upgrade\r\n") - nonce := generateNonce() - if config.handshakeData != nil { - nonce = []byte(config.handshakeData["key"]) - } - bw.WriteString("Sec-WebSocket-Key: " + string(nonce) + "\r\n") - bw.WriteString("Origin: " + strings.ToLower(config.Origin.String()) + "\r\n") - - if config.Version != ProtocolVersionHybi13 { - return ErrBadProtocolVersion - } - - bw.WriteString("Sec-WebSocket-Version: " + fmt.Sprintf("%d", config.Version) + "\r\n") - if len(config.Protocol) > 0 { - bw.WriteString("Sec-WebSocket-Protocol: " + strings.Join(config.Protocol, ", ") + "\r\n") - } - // TODO(ukai): send Sec-WebSocket-Extensions. - err = config.Header.WriteSubset(bw, handshakeHeader) - if err != nil { - return err - } - - bw.WriteString("\r\n") - if err = bw.Flush(); err != nil { - return err - } - - resp, err := http.ReadResponse(br, &http.Request{Method: "GET"}) - if err != nil { - return err - } - if resp.StatusCode != 101 { - return ErrBadStatus - } - if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" || - strings.ToLower(resp.Header.Get("Connection")) != "upgrade" { - return ErrBadUpgrade - } - expectedAccept, err := getNonceAccept(nonce) - if err != nil { - return err - } - if resp.Header.Get("Sec-WebSocket-Accept") != string(expectedAccept) { - return ErrChallengeResponse - } - if resp.Header.Get("Sec-WebSocket-Extensions") != "" { - return ErrUnsupportedExtensions - } - offeredProtocol := resp.Header.Get("Sec-WebSocket-Protocol") - if offeredProtocol != "" { - protocolMatched := false - for i := 0; i < len(config.Protocol); i++ { - if config.Protocol[i] == offeredProtocol { - protocolMatched = true - break - } - } - if !protocolMatched { - return ErrBadWebSocketProtocol - } - config.Protocol = []string{offeredProtocol} - } - - return nil -} - -// newHybiClientConn creates a client WebSocket connection after handshake. -func newHybiClientConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser) *Conn { - return newHybiConn(config, buf, rwc, nil) -} - -// A HybiServerHandshaker performs a server handshake using hybi draft protocol. -type hybiServerHandshaker struct { - *Config - accept []byte -} - -func (c *hybiServerHandshaker) ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) { - c.Version = ProtocolVersionHybi13 - if req.Method != "GET" { - return http.StatusMethodNotAllowed, ErrBadRequestMethod - } - // HTTP version can be safely ignored. - - if strings.ToLower(req.Header.Get("Upgrade")) != "websocket" || - !strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") { - return http.StatusBadRequest, ErrNotWebSocket - } - - key := req.Header.Get("Sec-Websocket-Key") - if key == "" { - return http.StatusBadRequest, ErrChallengeResponse - } - version := req.Header.Get("Sec-Websocket-Version") - switch version { - case "13": - c.Version = ProtocolVersionHybi13 - default: - return http.StatusBadRequest, ErrBadWebSocketVersion - } - var scheme string - if req.TLS != nil { - scheme = "wss" - } else { - scheme = "ws" - } - c.Location, err = url.ParseRequestURI(scheme + "://" + req.Host + req.URL.RequestURI()) - if err != nil { - return http.StatusBadRequest, err - } - protocol := strings.TrimSpace(req.Header.Get("Sec-Websocket-Protocol")) - if protocol != "" { - protocols := strings.Split(protocol, ",") - for i := 0; i < len(protocols); i++ { - c.Protocol = append(c.Protocol, strings.TrimSpace(protocols[i])) - } - } - c.accept, err = getNonceAccept([]byte(key)) - if err != nil { - return http.StatusInternalServerError, err - } - return http.StatusSwitchingProtocols, nil -} - -// Origin parses the Origin header in req. -// If the Origin header is not set, it returns nil and nil. -func Origin(config *Config, req *http.Request) (*url.URL, error) { - var origin string - switch config.Version { - case ProtocolVersionHybi13: - origin = req.Header.Get("Origin") - } - if origin == "" { - return nil, nil - } - return url.ParseRequestURI(origin) -} - -func (c *hybiServerHandshaker) AcceptHandshake(buf *bufio.Writer) (err error) { - if len(c.Protocol) > 0 { - if len(c.Protocol) != 1 { - // You need choose a Protocol in Handshake func in Server. - return ErrBadWebSocketProtocol - } - } - buf.WriteString("HTTP/1.1 101 Switching Protocols\r\n") - buf.WriteString("Upgrade: websocket\r\n") - buf.WriteString("Connection: Upgrade\r\n") - buf.WriteString("Sec-WebSocket-Accept: " + string(c.accept) + "\r\n") - if len(c.Protocol) > 0 { - buf.WriteString("Sec-WebSocket-Protocol: " + c.Protocol[0] + "\r\n") - } - // TODO(ukai): send Sec-WebSocket-Extensions. - if c.Header != nil { - err := c.Header.WriteSubset(buf, handshakeHeader) - if err != nil { - return err - } - } - buf.WriteString("\r\n") - return buf.Flush() -} - -func (c *hybiServerHandshaker) NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { - return newHybiServerConn(c.Config, buf, rwc, request) -} - -// newHybiServerConn returns a new WebSocket connection speaking hybi draft protocol. -func newHybiServerConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { - return newHybiConn(config, buf, rwc, request) -} diff --git a/vendor/golang.org/x/net/websocket/server.go b/vendor/golang.org/x/net/websocket/server.go deleted file mode 100644 index 0895dea..0000000 --- a/vendor/golang.org/x/net/websocket/server.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2009 The Go 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 ( - "bufio" - "fmt" - "io" - "net/http" -) - -func newServerConn(rwc io.ReadWriteCloser, buf *bufio.ReadWriter, req *http.Request, config *Config, handshake func(*Config, *http.Request) error) (conn *Conn, err error) { - var hs serverHandshaker = &hybiServerHandshaker{Config: config} - code, err := hs.ReadHandshake(buf.Reader, req) - if err == ErrBadWebSocketVersion { - fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) - fmt.Fprintf(buf, "Sec-WebSocket-Version: %s\r\n", SupportedProtocolVersion) - buf.WriteString("\r\n") - buf.WriteString(err.Error()) - buf.Flush() - return - } - if err != nil { - fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) - buf.WriteString("\r\n") - buf.WriteString(err.Error()) - buf.Flush() - return - } - if handshake != nil { - err = handshake(config, req) - if err != nil { - code = http.StatusForbidden - fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) - buf.WriteString("\r\n") - buf.Flush() - return - } - } - err = hs.AcceptHandshake(buf.Writer) - if err != nil { - code = http.StatusBadRequest - fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) - buf.WriteString("\r\n") - buf.Flush() - return - } - conn = hs.NewServerConn(buf, rwc, req) - return -} - -// Server represents a server of a WebSocket. -type Server struct { - // Config is a WebSocket configuration for new WebSocket connection. - Config - - // Handshake is an optional function in WebSocket handshake. - // For example, you can check, or don't check Origin header. - // Another example, you can select config.Protocol. - Handshake func(*Config, *http.Request) error - - // Handler handles a WebSocket connection. - Handler -} - -// ServeHTTP implements the http.Handler interface for a WebSocket -func (s Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { - s.serveWebSocket(w, req) -} - -func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) { - rwc, buf, err := w.(http.Hijacker).Hijack() - if err != nil { - panic("Hijack failed: " + err.Error()) - } - // The server should abort the WebSocket connection if it finds - // the client did not send a handshake that matches with protocol - // specification. - defer rwc.Close() - conn, err := newServerConn(rwc, buf, req, &s.Config, s.Handshake) - if err != nil { - return - } - if conn == nil { - panic("unexpected nil conn") - } - s.Handler(conn) -} - -// Handler is a simple interface to a WebSocket browser client. -// It checks if Origin header is valid URL by default. -// You might want to verify websocket.Conn.Config().Origin in the func. -// If you use Server instead of Handler, you could call websocket.Origin and -// check the origin in your Handshake func. So, if you want to accept -// non-browser clients, which do not send an Origin header, set a -// Server.Handshake that does not check the origin. -type Handler func(*Conn) - -func checkOrigin(config *Config, req *http.Request) (err error) { - config.Origin, err = Origin(config, req) - if err == nil && config.Origin == nil { - return fmt.Errorf("null origin") - } - return err -} - -// ServeHTTP implements the http.Handler interface for a WebSocket -func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - s := Server{Handler: h, Handshake: checkOrigin} - s.serveWebSocket(w, req) -} diff --git a/vendor/golang.org/x/net/websocket/websocket.go b/vendor/golang.org/x/net/websocket/websocket.go deleted file mode 100644 index e242c89..0000000 --- a/vendor/golang.org/x/net/websocket/websocket.go +++ /dev/null @@ -1,448 +0,0 @@ -// Copyright 2009 The Go 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 implements a client and server for the WebSocket protocol -// as specified in RFC 6455. -// -// This package currently lacks some features found in an alternative -// and more actively maintained WebSocket package: -// -// https://godoc.org/github.com/gorilla/websocket -// -package websocket // import "golang.org/x/net/websocket" - -import ( - "bufio" - "crypto/tls" - "encoding/json" - "errors" - "io" - "io/ioutil" - "net" - "net/http" - "net/url" - "sync" - "time" -) - -const ( - ProtocolVersionHybi13 = 13 - ProtocolVersionHybi = ProtocolVersionHybi13 - SupportedProtocolVersion = "13" - - ContinuationFrame = 0 - TextFrame = 1 - BinaryFrame = 2 - CloseFrame = 8 - PingFrame = 9 - PongFrame = 10 - UnknownFrame = 255 - - DefaultMaxPayloadBytes = 32 << 20 // 32MB -) - -// ProtocolError represents WebSocket protocol errors. -type ProtocolError struct { - ErrorString string -} - -func (err *ProtocolError) Error() string { return err.ErrorString } - -var ( - ErrBadProtocolVersion = &ProtocolError{"bad protocol version"} - ErrBadScheme = &ProtocolError{"bad scheme"} - ErrBadStatus = &ProtocolError{"bad status"} - ErrBadUpgrade = &ProtocolError{"missing or bad upgrade"} - ErrBadWebSocketOrigin = &ProtocolError{"missing or bad WebSocket-Origin"} - ErrBadWebSocketLocation = &ProtocolError{"missing or bad WebSocket-Location"} - ErrBadWebSocketProtocol = &ProtocolError{"missing or bad WebSocket-Protocol"} - ErrBadWebSocketVersion = &ProtocolError{"missing or bad WebSocket Version"} - ErrChallengeResponse = &ProtocolError{"mismatch challenge/response"} - ErrBadFrame = &ProtocolError{"bad frame"} - ErrBadFrameBoundary = &ProtocolError{"not on frame boundary"} - ErrNotWebSocket = &ProtocolError{"not websocket protocol"} - ErrBadRequestMethod = &ProtocolError{"bad method"} - ErrNotSupported = &ProtocolError{"not supported"} -) - -// ErrFrameTooLarge is returned by Codec's Receive method if payload size -// exceeds limit set by Conn.MaxPayloadBytes -var ErrFrameTooLarge = errors.New("websocket: frame payload size exceeds limit") - -// Addr is an implementation of net.Addr for WebSocket. -type Addr struct { - *url.URL -} - -// Network returns the network type for a WebSocket, "websocket". -func (addr *Addr) Network() string { return "websocket" } - -// Config is a WebSocket configuration -type Config struct { - // A WebSocket server address. - Location *url.URL - - // A Websocket client origin. - Origin *url.URL - - // WebSocket subprotocols. - Protocol []string - - // WebSocket protocol version. - Version int - - // TLS config for secure WebSocket (wss). - TlsConfig *tls.Config - - // Additional header fields to be sent in WebSocket opening handshake. - Header http.Header - - // Dialer used when opening websocket connections. - Dialer *net.Dialer - - handshakeData map[string]string -} - -// serverHandshaker is an interface to handle WebSocket server side handshake. -type serverHandshaker interface { - // ReadHandshake reads handshake request message from client. - // Returns http response code and error if any. - ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) - - // AcceptHandshake accepts the client handshake request and sends - // handshake response back to client. - AcceptHandshake(buf *bufio.Writer) (err error) - - // NewServerConn creates a new WebSocket connection. - NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) (conn *Conn) -} - -// frameReader is an interface to read a WebSocket frame. -type frameReader interface { - // Reader is to read payload of the frame. - io.Reader - - // PayloadType returns payload type. - PayloadType() byte - - // HeaderReader returns a reader to read header of the frame. - HeaderReader() io.Reader - - // TrailerReader returns a reader to read trailer of the frame. - // If it returns nil, there is no trailer in the frame. - TrailerReader() io.Reader - - // Len returns total length of the frame, including header and trailer. - Len() int -} - -// frameReaderFactory is an interface to creates new frame reader. -type frameReaderFactory interface { - NewFrameReader() (r frameReader, err error) -} - -// frameWriter is an interface to write a WebSocket frame. -type frameWriter interface { - // Writer is to write payload of the frame. - io.WriteCloser -} - -// frameWriterFactory is an interface to create new frame writer. -type frameWriterFactory interface { - NewFrameWriter(payloadType byte) (w frameWriter, err error) -} - -type frameHandler interface { - HandleFrame(frame frameReader) (r frameReader, err error) - WriteClose(status int) (err error) -} - -// Conn represents a WebSocket connection. -// -// Multiple goroutines may invoke methods on a Conn simultaneously. -type Conn struct { - config *Config - request *http.Request - - buf *bufio.ReadWriter - rwc io.ReadWriteCloser - - rio sync.Mutex - frameReaderFactory - frameReader - - wio sync.Mutex - frameWriterFactory - - frameHandler - PayloadType byte - defaultCloseStatus int - - // MaxPayloadBytes limits the size of frame payload received over Conn - // by Codec's Receive method. If zero, DefaultMaxPayloadBytes is used. - MaxPayloadBytes int -} - -// Read implements the io.Reader interface: -// it reads data of a frame from the WebSocket connection. -// if msg is not large enough for the frame data, it fills the msg and next Read -// will read the rest of the frame data. -// it reads Text frame or Binary frame. -func (ws *Conn) Read(msg []byte) (n int, err error) { - ws.rio.Lock() - defer ws.rio.Unlock() -again: - if ws.frameReader == nil { - frame, err := ws.frameReaderFactory.NewFrameReader() - if err != nil { - return 0, err - } - ws.frameReader, err = ws.frameHandler.HandleFrame(frame) - if err != nil { - return 0, err - } - if ws.frameReader == nil { - goto again - } - } - n, err = ws.frameReader.Read(msg) - if err == io.EOF { - if trailer := ws.frameReader.TrailerReader(); trailer != nil { - io.Copy(ioutil.Discard, trailer) - } - ws.frameReader = nil - goto again - } - return n, err -} - -// Write implements the io.Writer interface: -// it writes data as a frame to the WebSocket connection. -func (ws *Conn) Write(msg []byte) (n int, err error) { - ws.wio.Lock() - defer ws.wio.Unlock() - w, err := ws.frameWriterFactory.NewFrameWriter(ws.PayloadType) - if err != nil { - return 0, err - } - n, err = w.Write(msg) - w.Close() - return n, err -} - -// Close implements the io.Closer interface. -func (ws *Conn) Close() error { - err := ws.frameHandler.WriteClose(ws.defaultCloseStatus) - err1 := ws.rwc.Close() - if err != nil { - return err - } - return err1 -} - -func (ws *Conn) IsClientConn() bool { return ws.request == nil } -func (ws *Conn) IsServerConn() bool { return ws.request != nil } - -// LocalAddr returns the WebSocket Origin for the connection for client, or -// the WebSocket location for server. -func (ws *Conn) LocalAddr() net.Addr { - if ws.IsClientConn() { - return &Addr{ws.config.Origin} - } - return &Addr{ws.config.Location} -} - -// RemoteAddr returns the WebSocket location for the connection for client, or -// the Websocket Origin for server. -func (ws *Conn) RemoteAddr() net.Addr { - if ws.IsClientConn() { - return &Addr{ws.config.Location} - } - return &Addr{ws.config.Origin} -} - -var errSetDeadline = errors.New("websocket: cannot set deadline: not using a net.Conn") - -// SetDeadline sets the connection's network read & write deadlines. -func (ws *Conn) SetDeadline(t time.Time) error { - if conn, ok := ws.rwc.(net.Conn); ok { - return conn.SetDeadline(t) - } - return errSetDeadline -} - -// SetReadDeadline sets the connection's network read deadline. -func (ws *Conn) SetReadDeadline(t time.Time) error { - if conn, ok := ws.rwc.(net.Conn); ok { - return conn.SetReadDeadline(t) - } - return errSetDeadline -} - -// SetWriteDeadline sets the connection's network write deadline. -func (ws *Conn) SetWriteDeadline(t time.Time) error { - if conn, ok := ws.rwc.(net.Conn); ok { - return conn.SetWriteDeadline(t) - } - return errSetDeadline -} - -// Config returns the WebSocket config. -func (ws *Conn) Config() *Config { return ws.config } - -// Request returns the http request upgraded to the WebSocket. -// It is nil for client side. -func (ws *Conn) Request() *http.Request { return ws.request } - -// Codec represents a symmetric pair of functions that implement a codec. -type Codec struct { - Marshal func(v interface{}) (data []byte, payloadType byte, err error) - Unmarshal func(data []byte, payloadType byte, v interface{}) (err error) -} - -// Send sends v marshaled by cd.Marshal as single frame to ws. -func (cd Codec) Send(ws *Conn, v interface{}) (err error) { - data, payloadType, err := cd.Marshal(v) - if err != nil { - return err - } - ws.wio.Lock() - defer ws.wio.Unlock() - w, err := ws.frameWriterFactory.NewFrameWriter(payloadType) - if err != nil { - return err - } - _, err = w.Write(data) - w.Close() - return err -} - -// Receive receives single frame from ws, unmarshaled by cd.Unmarshal and stores -// in v. The whole frame payload is read to an in-memory buffer; max size of -// payload is defined by ws.MaxPayloadBytes. If frame payload size exceeds -// limit, ErrFrameTooLarge is returned; in this case frame is not read off wire -// completely. The next call to Receive would read and discard leftover data of -// previous oversized frame before processing next frame. -func (cd Codec) Receive(ws *Conn, v interface{}) (err error) { - ws.rio.Lock() - defer ws.rio.Unlock() - if ws.frameReader != nil { - _, err = io.Copy(ioutil.Discard, ws.frameReader) - if err != nil { - return err - } - ws.frameReader = nil - } -again: - frame, err := ws.frameReaderFactory.NewFrameReader() - if err != nil { - return err - } - frame, err = ws.frameHandler.HandleFrame(frame) - if err != nil { - return err - } - if frame == nil { - goto again - } - maxPayloadBytes := ws.MaxPayloadBytes - if maxPayloadBytes == 0 { - maxPayloadBytes = DefaultMaxPayloadBytes - } - if hf, ok := frame.(*hybiFrameReader); ok && hf.header.Length > int64(maxPayloadBytes) { - // payload size exceeds limit, no need to call Unmarshal - // - // set frameReader to current oversized frame so that - // the next call to this function can drain leftover - // data before processing the next frame - ws.frameReader = frame - return ErrFrameTooLarge - } - payloadType := frame.PayloadType() - data, err := ioutil.ReadAll(frame) - if err != nil { - return err - } - return cd.Unmarshal(data, payloadType, v) -} - -func marshal(v interface{}) (msg []byte, payloadType byte, err error) { - switch data := v.(type) { - case string: - return []byte(data), TextFrame, nil - case []byte: - return data, BinaryFrame, nil - } - return nil, UnknownFrame, ErrNotSupported -} - -func unmarshal(msg []byte, payloadType byte, v interface{}) (err error) { - switch data := v.(type) { - case *string: - *data = string(msg) - return nil - case *[]byte: - *data = msg - return nil - } - return ErrNotSupported -} - -/* -Message is a codec to send/receive text/binary data in a frame on WebSocket connection. -To send/receive text frame, use string type. -To send/receive binary frame, use []byte type. - -Trivial usage: - - import "websocket" - - // receive text frame - var message string - websocket.Message.Receive(ws, &message) - - // send text frame - message = "hello" - websocket.Message.Send(ws, message) - - // receive binary frame - var data []byte - websocket.Message.Receive(ws, &data) - - // send binary frame - data = []byte{0, 1, 2} - websocket.Message.Send(ws, data) - -*/ -var Message = Codec{marshal, unmarshal} - -func jsonMarshal(v interface{}) (msg []byte, payloadType byte, err error) { - msg, err = json.Marshal(v) - return msg, TextFrame, err -} - -func jsonUnmarshal(msg []byte, payloadType byte, v interface{}) (err error) { - return json.Unmarshal(msg, v) -} - -/* -JSON is a codec to send/receive JSON data in a frame from a WebSocket connection. - -Trivial usage: - - import "websocket" - - type T struct { - Msg string - Count int - } - - // receive JSON type T - var data T - websocket.JSON.Receive(ws, &data) - - // send JSON type T - websocket.JSON.Send(ws, data) -*/ -var JSON = Codec{jsonMarshal, jsonUnmarshal} diff --git a/vendor/vendor.json b/vendor/vendor.json deleted file mode 100644 index a246ab4..0000000 --- a/vendor/vendor.json +++ /dev/null @@ -1,243 +0,0 @@ -{ - "comment": "", - "ignore": "test", - "package": [ - { - "path": "bufio", - "revision": "" - }, - { - "path": "bytes", - "revision": "" - }, - { - "path": "context", - "revision": "" - }, - { - "path": "crypto/rand", - "revision": "" - }, - { - "path": "crypto/sha1", - "revision": "" - }, - { - "path": "crypto/tls", - "revision": "" - }, - { - "path": "encoding/base64", - "revision": "" - }, - { - "path": "encoding/binary", - "revision": "" - }, - { - "path": "encoding/hex", - "revision": "" - }, - { - "path": "encoding/json", - "revision": "" - }, - { - "path": "errors", - "revision": "" - }, - { - "path": "flag", - "revision": "" - }, - { - "path": "fmt", - "revision": "" - }, - { - "checksumSHA1": "CbpC2ha+GTTuROMyyLVd/L3O+8Y=", - "path": "github.com/erroneousboat/termui", - "revision": "80f245cdfa0488883a3e8602bf3f0c8a3c889a22", - "revisionTime": "2017-09-23T11:51:41Z" - }, - { - "checksumSHA1": "zpFCi2nWiwR5F2INAJOvQqsj7lY=", - "path": "github.com/maruel/panicparse/stack", - "revision": "766956aceb8ff49664065ae50bef0ae8a0a83ec4", - "revisionTime": "2017-11-29T15:16:18Z" - }, - { - "checksumSHA1": "cJE7dphDlam/i7PhnsyosNWtbd4=", - "path": "github.com/mattn/go-runewidth", - "revision": "97311d9f7767e3d6f422ea06661bc2c7a19e8a5d", - "revisionTime": "2017-05-10T07:48:58Z" - }, - { - "checksumSHA1": "L3leymg2RT8hFl5uL+5KP/LpBkg=", - "path": "github.com/mitchellh/go-wordwrap", - "revision": "ad45545899c7b13c020ea92b2072220eefad42b8", - "revisionTime": "2015-03-14T17:03:34Z" - }, - { - "checksumSHA1": "HYgTWn4FgVbvSBYVO4DxUPWfCz0=", - "path": "github.com/nlopes/slack", - "revision": "5cde21b8b96a43fc3435a1f514123d14fd7eabdc", - "revisionTime": "2017-07-25T12:17:30Z" - }, - { - "checksumSHA1": "Zi8hWUMkKtii1fc6YaGgoYAssIw=", - "path": "github.com/nsf/termbox-go", - "revision": "aa4a75b1c20a2b03751b1a9f7e41d58bd6f71c43", - "revisionTime": "2017-11-04T16:23:16Z" - }, - { - "checksumSHA1": "DF3jZEw4lCq/SEaC7DIl/R+7S70=", - "path": "github.com/renstrom/fuzzysearch/fuzzy", - "revision": "2d205ac6ec17a839a94bdbfd16d2fa6c6dada2e0", - "revisionTime": "2016-03-31T20:48:55Z" - }, - { - "path": "go/ast", - "revision": "" - }, - { - "path": "go/parser", - "revision": "" - }, - { - "path": "go/token", - "revision": "" - }, - { - "checksumSHA1": "7EZyXN0EmZLgGxZxK01IJua4c8o=", - "path": "golang.org/x/net/websocket", - "revision": "a8b9294777976932365dabb6640cf1468d95c70f", - "revisionTime": "2017-11-29T19:21:16Z" - }, - { - "path": "html", - "revision": "" - }, - { - "path": "image", - "revision": "" - }, - { - "path": "io", - "revision": "" - }, - { - "path": "io/ioutil", - "revision": "" - }, - { - "path": "log", - "revision": "" - }, - { - "path": "math", - "revision": "" - }, - { - "path": "math/rand", - "revision": "" - }, - { - "path": "mime/multipart", - "revision": "" - }, - { - "path": "net", - "revision": "" - }, - { - "path": "net/http", - "revision": "" - }, - { - "path": "net/http/httputil", - "revision": "" - }, - { - "path": "net/url", - "revision": "" - }, - { - "path": "os", - "revision": "" - }, - { - "path": "os/signal", - "revision": "" - }, - { - "path": "os/user", - "revision": "" - }, - { - "path": "path", - "revision": "" - }, - { - "path": "path/filepath", - "revision": "" - }, - { - "path": "reflect", - "revision": "" - }, - { - "path": "regexp", - "revision": "" - }, - { - "path": "runtime", - "revision": "" - }, - { - "path": "runtime/debug", - "revision": "" - }, - { - "path": "sort", - "revision": "" - }, - { - "path": "strconv", - "revision": "" - }, - { - "path": "strings", - "revision": "" - }, - { - "path": "sync", - "revision": "" - }, - { - "path": "syscall", - "revision": "" - }, - { - "path": "time", - "revision": "" - }, - { - "path": "unicode", - "revision": "" - }, - { - "path": "unicode/utf16", - "revision": "" - }, - { - "path": "unicode/utf8", - "revision": "" - }, - { - "path": "unsafe", - "revision": "" - } - ], - "rootPath": "github.com/erroneousboat/slack-term" -} From cae6a9a66263f79bd33586568b9f8e269ce6747a Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Fri, 23 Mar 2018 12:26:09 +0100 Subject: [PATCH 02/12] Fix panic when pasting in normal mode Fixes #120 --- handlers/event.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/handlers/event.go b/handlers/event.go index 556c46b..965526a 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -233,17 +233,21 @@ func actionSend(ctx *context.AppContext) { } } +// actionSearch will search through the channels based on the users +// input. A time is implemented to make sure the actual searching +// and changing of channels is done when the user's typing is paused. func actionSearch(ctx *context.AppContext, key rune) { + actionInput(ctx.View, key) + go func() { if timer != nil { timer.Stop() } - actionInput(ctx.View, key) - timer = time.NewTimer(time.Second / 4) <-timer.C + // Only actually search when the time expires term := ctx.View.Input.GetText() ctx.View.Channels.Search(term) actionChangeChannel(ctx) @@ -291,7 +295,7 @@ func actionGetMessages(ctx *context.AppContext) { } // actionMoveCursorUpChannels will execute the actionChangeChannel -// function. A time is implemented to support fast scrolling through +// function. A timer is implemented to support fast scrolling through // the list without executing the actionChangeChannel event func actionMoveCursorUpChannels(ctx *context.AppContext) { go func() { @@ -311,7 +315,7 @@ func actionMoveCursorUpChannels(ctx *context.AppContext) { } // actionMoveCursorDownChannels will execute the actionChangeChannel -// function. A time is implemented to support fast scrolling through +// function. A timer is implemented to support fast scrolling through // the list without executing the actionChangeChannel event func actionMoveCursorDownChannels(ctx *context.AppContext) { go func() { From ad04a301ec03974defc3c7fe8fa593d7d90ab209 Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Fri, 23 Mar 2018 12:49:23 +0100 Subject: [PATCH 03/12] Make default slack-term config file a dotfile Fixes #118, #127 --- config/config.go | 5 +++-- main.go | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config/config.go b/config/config.go index 4a1ece9..8e4e33e 100644 --- a/config/config.go +++ b/config/config.go @@ -3,6 +3,7 @@ package config import ( "encoding/json" "errors" + "fmt" "os" "github.com/erroneousboat/termui" @@ -25,11 +26,11 @@ func NewConfig(filepath string) (*Config, error) { file, err := os.Open(filepath) if err != nil { - return &cfg, err + return &cfg, fmt.Errorf("couldn't find the slack-term config file: %v", err) } if err := json.NewDecoder(file).Decode(&cfg); err != nil { - return &cfg, err + return &cfg, fmt.Errorf("the slack-term config file isn't valid json: %v", err) } if cfg.SlackToken == "" { diff --git a/main.go b/main.go index d862702..61f5588 100644 --- a/main.go +++ b/main.go @@ -51,7 +51,7 @@ func init() { flag.StringVar( &flgConfig, "config", - path.Join(usr.HomeDir, "slack-term.json"), + path.Join(usr.HomeDir, ".slack-term"), "location of config file", ) From e6897a6c405aa0a145f4ff55baa3c3e8ac36201d Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Fri, 23 Mar 2018 13:08:42 +0100 Subject: [PATCH 04/12] Limit resizing functionality Fixes #88 --- handlers/event.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/handlers/event.go b/handlers/event.go index 965526a..06338f1 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -162,6 +162,12 @@ func actionKeyEvent(ctx *context.AppContext, ev termbox.Event) { } func actionResizeEvent(ctx *context.AppContext, ev termbox.Event) { + // When terminal window is too small termui will panic, here + // we won't resize when the terminal window is too small. + if termui.TermWidth() < 25 || termui.TermHeight() < 5 { + return + } + termui.Body.Width = termui.TermWidth() // Vertical resize components From f1f0bc13792d68df7fe942bfe235000fc87c6aa3 Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Sat, 24 Mar 2018 13:56:55 +0100 Subject: [PATCH 05/12] Ignore reply events --- handlers/event.go | 8 +++++++- service/slack.go | 12 ++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/handlers/event.go b/handlers/event.go index 06338f1..172539e 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -47,6 +47,7 @@ func RegisterEventHandlers(ctx *context.AppContext) { messageHandler(ctx) } +// eventHandler will handle events created by the user func eventHandler(ctx *context.AppContext) { go func() { for { @@ -95,6 +96,7 @@ func handleMoreTermboxEvents(ctx *context.AppContext, ev termbox.Event) bool { } } +// messageHandler will handle events created by the service func messageHandler(ctx *context.AppContext) { go func() { for { @@ -102,8 +104,12 @@ func messageHandler(ctx *context.AppContext) { case msg := <-ctx.Service.RTM.IncomingEvents: switch ev := msg.Data.(type) { case *slack.MessageEvent: + // Construct message - msg := ctx.Service.CreateMessageFromMessageEvent(ev) + msg, err := ctx.Service.CreateMessageFromMessageEvent(ev) + if err != nil { + continue + } // Add message to the selected channel if ev.Channel == ctx.Service.Channels[ctx.View.Channels.SelectedChannel].ID { diff --git a/service/slack.go b/service/slack.go index 5424226..5169776 100644 --- a/service/slack.go +++ b/service/slack.go @@ -397,15 +397,19 @@ func (s *SlackService) CreateMessage(message slack.Message) []components.Message return msgs } -func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent) []components.Message { +func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent) ([]components.Message, error) { var msgs []components.Message var name string - // Append (edited) when an edited message is received - if message.SubType == "message_changed" { + switch message.SubType { + case "message_changed": + // Append (edited) when an edited message is received message = &slack.MessageEvent{Msg: *message.SubMessage} message.Text = fmt.Sprintf("%s (edited)", message.Text) + case "message_replied": + // Ignore reply events + return nil, errors.New("ignoring reply events") } // Get username from cache @@ -462,7 +466,7 @@ func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent msgs = append(msgs, msg) - return msgs + return msgs, nil } // parseMessage will parse a message string and find and replace: From 29201a8bb28bc0a9f11b3d4ef50a289319bc57fc Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Fri, 30 Mar 2018 14:59:23 +0200 Subject: [PATCH 06/12] Fix termui version --- Gopkg.lock | 5 ++--- Gopkg.toml | 2 +- vendor/github.com/erroneousboat/termui/barchart.go | 2 +- vendor/github.com/erroneousboat/termui/events.go | 11 ++++++++--- vendor/github.com/erroneousboat/termui/grid.go | 4 ++-- vendor/github.com/erroneousboat/termui/helper.go | 2 +- vendor/github.com/erroneousboat/termui/linechart.go | 4 ++-- vendor/github.com/erroneousboat/termui/list.go | 2 +- vendor/github.com/erroneousboat/termui/mbarchart.go | 8 ++++---- vendor/github.com/erroneousboat/termui/render.go | 4 ++-- vendor/github.com/erroneousboat/termui/sparkline.go | 2 +- 11 files changed, 25 insertions(+), 21 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 84227fa..ee1098a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -4,8 +4,7 @@ [[projects]] name = "github.com/erroneousboat/termui" packages = ["."] - revision = "24acd523c756fd9728824cdfac66aad9d8982fb7" - version = "v2.2.0" + revision = "80f245cdfa0488883a3e8602bf3f0c8a3c889a22" [[projects]] name = "github.com/gorilla/websocket" @@ -51,6 +50,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "719f2440551009ce6f1e1a7f6e56db762acdadd9a0e05245ee427b7455e50233" + inputs-digest = "353a2a71e00ecf8dd6123a02828c450fa6d38472a98792d2d8a4cd6349900f11" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 1d9ea71..f7cbb2b 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -27,7 +27,7 @@ [[constraint]] name = "github.com/erroneousboat/termui" - version = "2.2.0" + revision = "80f245cdfa0488883a3e8602bf3f0c8a3c889a22" [[constraint]] name = "github.com/mattn/go-runewidth" diff --git a/vendor/github.com/erroneousboat/termui/barchart.go b/vendor/github.com/erroneousboat/termui/barchart.go index 6560c8b..d960797 100644 --- a/vendor/github.com/erroneousboat/termui/barchart.go +++ b/vendor/github.com/erroneousboat/termui/barchart.go @@ -62,7 +62,7 @@ func (bc *BarChart) layout() { } //bc.max = bc.Data[0] // what if Data is nil? Sometimes when bar graph is nill it produces panic with panic: runtime error: index out of range - // Asign a negative value to get maxvalue auto-populates + // Assign a negative value to get maxvalue auto-populates if bc.max == 0 { bc.max = -1 } diff --git a/vendor/github.com/erroneousboat/termui/events.go b/vendor/github.com/erroneousboat/termui/events.go index eb7319b..f0a45da 100644 --- a/vendor/github.com/erroneousboat/termui/events.go +++ b/vendor/github.com/erroneousboat/termui/events.go @@ -126,7 +126,7 @@ func hookTermboxEvt() { e := termbox.PollEvent() for _, c := range sysEvtChs { - go func(ch chan Event) { + func(ch chan Event) { ch <- crtTermboxEvt(e) }(c) } @@ -221,6 +221,7 @@ func findMatch(mux map[string]func(Event), path string) string { return pattern } + // Remove all existing defined Handlers from the map func (es *EvtStream) ResetHandlers() { for Path, _ := range es.Handlers { @@ -243,7 +244,7 @@ func (es *EvtStream) Loop() { case "/sig/stoploop": return } - go func(a Event) { + func(a Event) { es.RLock() defer es.RUnlock() if pattern := es.match(a.Path); pattern != "" { @@ -273,6 +274,10 @@ func Handle(path string, handler func(Event)) { DefaultEvtStream.Handle(path, handler) } +func ResetHandlers() { + DefaultEvtStream.ResetHandlers() +} + func Loop() { DefaultEvtStream.Loop() } @@ -309,7 +314,7 @@ func NewTimerCh(du time.Duration) chan Event { return t } -var DefualtHandler = func(e Event) { +var DefaultHandler = func(e Event) { } var usrEvtCh = make(chan Event) diff --git a/vendor/github.com/erroneousboat/termui/grid.go b/vendor/github.com/erroneousboat/termui/grid.go index a950232..851489d 100644 --- a/vendor/github.com/erroneousboat/termui/grid.go +++ b/vendor/github.com/erroneousboat/termui/grid.go @@ -48,7 +48,7 @@ func (r *Row) assignWidth(w int) { accW := 0 // acc span and offset calcW := make([]int, len(r.Cols)) // calculated width - calcOftX := make([]int, len(r.Cols)) // computated start position of x + calcOftX := make([]int, len(r.Cols)) // computed start position of x for i, c := range r.Cols { accW += c.Span + c.Offset @@ -266,7 +266,7 @@ func (g *Grid) Align() { } } -// Buffer implments Bufferer interface. +// Buffer implements Bufferer interface. func (g Grid) Buffer() Buffer { buf := NewBuffer() diff --git a/vendor/github.com/erroneousboat/termui/helper.go b/vendor/github.com/erroneousboat/termui/helper.go index 18a6770..5a71afb 100644 --- a/vendor/github.com/erroneousboat/termui/helper.go +++ b/vendor/github.com/erroneousboat/termui/helper.go @@ -100,7 +100,7 @@ func charWidth(ch rune) int { var whiteSpaceRegex = regexp.MustCompile(`\s`) -// StringToAttribute converts text to a termui attribute. You may specifiy more +// StringToAttribute converts text to a termui attribute. You may specify more // then one attribute like that: "BLACK, BOLD, ...". All whitespaces // are ignored. func StringToAttribute(text string) Attribute { diff --git a/vendor/github.com/erroneousboat/termui/linechart.go b/vendor/github.com/erroneousboat/termui/linechart.go index f7eea28..84e7e28 100644 --- a/vendor/github.com/erroneousboat/termui/linechart.go +++ b/vendor/github.com/erroneousboat/termui/linechart.go @@ -35,7 +35,7 @@ var braillePatterns = map[[2]int]rune{ var lSingleBraille = [4]rune{'\u2840', '⠄', '⠂', '⠁'} var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'} -// LineChart has two modes: braille(default) and dot. Using braille gives 2x capicity as dot mode, +// LineChart has two modes: braille(default) and dot. Using braille gives 2x capacity as dot mode, // because one braille char can represent two data points. /* lc := termui.NewLineChart() @@ -87,7 +87,7 @@ func NewLineChart() *LineChart { } // one cell contains two data points -// so the capicity is 2x as dot-mode +// so the capacity is 2x as dot-mode func (lc *LineChart) renderBraille() Buffer { buf := NewBuffer() diff --git a/vendor/github.com/erroneousboat/termui/list.go b/vendor/github.com/erroneousboat/termui/list.go index ea6635e..5a59215 100644 --- a/vendor/github.com/erroneousboat/termui/list.go +++ b/vendor/github.com/erroneousboat/termui/list.go @@ -14,7 +14,7 @@ import "strings" strs := []string{ "[0] github.com/gizak/termui", "[1] editbox.go", - "[2] iterrupt.go", + "[2] interrupt.go", "[3] keyboard.go", "[4] output.go", "[5] random_out.go", diff --git a/vendor/github.com/erroneousboat/termui/mbarchart.go b/vendor/github.com/erroneousboat/termui/mbarchart.go index 0f91e97..0ce6c45 100644 --- a/vendor/github.com/erroneousboat/termui/mbarchart.go +++ b/vendor/github.com/erroneousboat/termui/mbarchart.go @@ -8,7 +8,7 @@ import ( "fmt" ) -// This is the implemetation of multi-colored or stacked bar graph. This is different from default barGraph which is implemented in bar.go +// This is the implementation of multi-colored or stacked bar graph. This is different from default barGraph which is implemented in bar.go // Multi-Colored-BarChart creates multiple bars in a widget: /* bc := termui.NewMBarChart() @@ -72,7 +72,7 @@ func (bc *MBarChart) layout() { } bc.numStack = DataLen - //We need to know what is the mimimum size of data array data[0] could have 10 elements data[1] could have only 5, so we plot only 5 bar graphs + //We need to know what is the minimum size of data array data[0] could have 10 elements data[1] could have only 5, so we plot only 5 bar graphs for i := 0; i < DataLen; i++ { if bc.minDataLen > len(bc.Data[i]) { @@ -90,13 +90,13 @@ func (bc *MBarChart) layout() { for i := 0; i < bc.numStack; i++ { bc.dataNum[i] = make([][]rune, len(bc.Data[i])) - //For each stack of bar calcualte the rune + //For each stack of bar calculate the rune for j := 0; j < LabelLen && i < bc.numBar; j++ { n := bc.Data[i][j] s := fmt.Sprint(n) bc.dataNum[i][j] = trimStr2Runes(s, bc.BarWidth) } - //If color is not defined by default then populate a color that is different from the prevous bar + //If color is not defined by default then populate a color that is different from the previous bar if bc.BarColor[i] == ColorDefault && bc.NumColor[i] == ColorDefault { if i == 0 { bc.BarColor[i] = ColorBlack diff --git a/vendor/github.com/erroneousboat/termui/render.go b/vendor/github.com/erroneousboat/termui/render.go index 5b58409..4959c2a 100644 --- a/vendor/github.com/erroneousboat/termui/render.go +++ b/vendor/github.com/erroneousboat/termui/render.go @@ -35,7 +35,7 @@ func Init() error { } sysEvtChs = make([]chan Event, 0) - go hookTermboxEvt() + // go hookTermboxEvt() renderJobs = make(chan []Bufferer) //renderLock = new(sync.RWMutex) @@ -51,7 +51,7 @@ func Init() error { DefaultEvtStream.Merge("timer", NewTimerCh(time.Second)) DefaultEvtStream.Merge("custom", usrEvtCh) - DefaultEvtStream.Handle("/", DefualtHandler) + DefaultEvtStream.Handle("/", DefaultHandler) DefaultEvtStream.Handle("/sys/wnd/resize", func(e Event) { w := e.Data.(EvtWnd) Body.Width = w.Width diff --git a/vendor/github.com/erroneousboat/termui/sparkline.go b/vendor/github.com/erroneousboat/termui/sparkline.go index d906e49..75e7c52 100644 --- a/vendor/github.com/erroneousboat/termui/sparkline.go +++ b/vendor/github.com/erroneousboat/termui/sparkline.go @@ -51,7 +51,7 @@ func NewSparkline() Sparkline { LineColor: ThemeAttr("sparkline.line.fg")} } -// NewSparklines return a new *Spaklines with given Sparkline(s), you can always add a new Sparkline later. +// NewSparklines return a new *Sparklines with given Sparkline(s), you can always add a new Sparkline later. func NewSparklines(ss ...Sparkline) *Sparklines { s := &Sparklines{Block: *NewBlock(), Lines: ss} return s From 0197300b1858d528139d6355a1358cfd43d0ccbd Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Fri, 30 Mar 2018 15:55:29 +0200 Subject: [PATCH 07/12] Add loading screen --- context/context.go | 3 +++ views/load.go | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 views/load.go diff --git a/context/context.go b/context/context.go index 8638771..16f871d 100644 --- a/context/context.go +++ b/context/context.go @@ -37,6 +37,9 @@ func CreateAppContext(flgConfig string, flgDebug bool) (*AppContext, error) { }() } + // Loading screen + views.Loading() + // Load config config, err := config.NewConfig(flgConfig) if err != nil { diff --git a/views/load.go b/views/load.go new file mode 100644 index 0000000..93c6a0d --- /dev/null +++ b/views/load.go @@ -0,0 +1,21 @@ +package views + +import ( + termbox "github.com/nsf/termbox-go" +) + +func Loading() { + const loading string = "LOADING" + + w, h := termbox.Size() + termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) + + offset := (w / 2) - (len(loading) / 2) + y := h / 2 + + for x := 0; x < len(loading); x++ { + termbox.SetCell(offset+x, y, rune(loading[x]), termbox.ColorDefault, termbox.ColorDefault) + } + + termbox.Flush() +} From 2ccc92179b0d70a5d6b8adcb766a306b29b291ec Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Sat, 31 Mar 2018 10:58:37 +0200 Subject: [PATCH 08/12] Speed up process of loading channels --- service/slack.go | 84 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 17 deletions(-) diff --git a/service/slack.go b/service/slack.go index 5169776..67f54a3 100644 --- a/service/slack.go +++ b/service/slack.go @@ -7,6 +7,7 @@ import ( "regexp" "strconv" "strings" + "sync" "time" "github.com/nlopes/slack" @@ -81,12 +82,47 @@ func NewSlackService(config *config.Config) (*SlackService, error) { func (s *SlackService) GetChannels() []string { var chans []components.ChannelItem - // Channel - slackChans, err := s.Client.GetChannels(true) - if err != nil { - chans = append(chans, components.ChannelItem{}) - } + var wg sync.WaitGroup + // Channels + wg.Add(1) + var slackChans []slack.Channel + go func() { + var err error + slackChans, err = s.Client.GetChannels(true) + if err != nil { + chans = append(chans, components.ChannelItem{}) + } + wg.Done() + }() + + // Groups + wg.Add(1) + var slackGroups []slack.Group + go func() { + var err error + slackGroups, err = s.Client.GetGroups(true) + if err != nil { + chans = append(chans, components.ChannelItem{}) + } + wg.Done() + }() + + // IM + wg.Add(1) + var slackIM []slack.IM + go func() { + var err error + slackIM, err = s.Client.GetIMChannels() + if err != nil { + chans = append(chans, components.ChannelItem{}) + } + wg.Done() + }() + + wg.Wait() + + // Channels for _, chn := range slackChans { if chn.IsMember { s.SlackChannels = append(s.SlackChannels, chn) @@ -106,10 +142,6 @@ func (s *SlackService) GetChannels() []string { } // Groups - slackGroups, err := s.Client.GetGroups(true) - if err != nil { - chans = append(chans, components.ChannelItem{}) - } for _, grp := range slackGroups { s.SlackChannels = append(s.SlackChannels, grp) chans = append( @@ -127,15 +159,8 @@ func (s *SlackService) GetChannels() []string { } // IM - slackIM, err := s.Client.GetIMChannels() - if err != nil { - chans = append(chans, components.ChannelItem{}) - } for _, im := range slackIM { - // FIXME: err - presence, _ := s.GetUserPresence(im.User) - // Uncover name, when we can't uncover name for // IM channel this is then probably a deleted // user, because we won't add deleted users @@ -151,7 +176,7 @@ func (s *SlackService) GetChannels() []string { Topic: "", Type: components.ChannelTypeIM, UserID: im.User, - Presence: presence, + Presence: "", StylePrefix: s.Config.Theme.Channel.Prefix, StyleIcon: s.Config.Theme.Channel.Icon, StyleText: s.Config.Theme.Channel.Text, @@ -163,10 +188,15 @@ func (s *SlackService) GetChannels() []string { s.Channels = chans + // We set presence of IM channels here because we need to separately + // issue an API call for every channel, this will speed up that process + s.SetPresenceChannels() + var channels []string for _, chn := range s.Channels { channels = append(channels, chn.ToString()) } + return channels } @@ -179,6 +209,26 @@ func (s *SlackService) ChannelsToString() []string { return channels } +// SetPresence will set presence for all IM channels +func (s *SlackService) SetPresenceChannels() { + var wg sync.WaitGroup + for i, channel := range s.SlackChannels { + + switch channel := channel.(type) { + case slack.IM: + wg.Add(1) + go func(i int) { + presence, _ := s.GetUserPresence(channel.User) + s.Channels[i].Presence = presence + wg.Done() + }(i) + } + + } + + wg.Wait() +} + // SetPresenceChannelEvent will set the presence of a IM channel func (s *SlackService) SetPresenceChannelEvent(userID string, presence string) { // Get the correct Channel from svc.Channels From 50114764a799e956b67325072ef69c9e3906271d Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Sun, 1 Apr 2018 13:03:28 +0200 Subject: [PATCH 09/12] Add desktop notifications Fixes #116 --- Gopkg.lock | 8 +- README.md | 8 +- config/config.go | 2 + context/context.go | 3 + handlers/event.go | 51 ++++-- service/slack.go | 32 +++- vendor/github.com/0xAX/notificator/.gitignore | 25 +++ vendor/github.com/0xAX/notificator/LICENSE | 27 +++ vendor/github.com/0xAX/notificator/README.md | 49 ++++++ .../0xAX/notificator/notification.go | 166 ++++++++++++++++++ 10 files changed, 352 insertions(+), 19 deletions(-) create mode 100644 vendor/github.com/0xAX/notificator/.gitignore create mode 100644 vendor/github.com/0xAX/notificator/LICENSE create mode 100644 vendor/github.com/0xAX/notificator/README.md create mode 100644 vendor/github.com/0xAX/notificator/notification.go diff --git a/Gopkg.lock b/Gopkg.lock index ee1098a..f5cfb64 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,12 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + branch = "master" + name = "github.com/0xAX/notificator" + packages = ["."] + revision = "88d57ee9043ba88d6a62e437fa15dda1ca0d2b59" + [[projects]] name = "github.com/erroneousboat/termui" packages = ["."] @@ -50,6 +56,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "353a2a71e00ecf8dd6123a02828c450fa6d38472a98792d2d8a4cd6349900f11" + inputs-digest = "6cdfd0125aad6371a6f4e75c7fc29507cee4a6001a6c68e06c7237066a31153a" solver-name = "gps-cdcl" solver-version = 1 diff --git a/README.md b/README.md index 3803d68..bf58487 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Slack-Term +slack-term ========== A [Slack](https://slack.com) client for your terminal. @@ -42,7 +42,11 @@ Setup "slack_token": "yourslacktokenhere", // OPTIONAL: set the width of the sidebar (between 1 and 11), default is 1 - "sidebar_width": 3, + "sidebar_width": 1, + + // OPTIONAL: turn on desktop notifications for all incoming messages, + default is false + "notify": false, // OPTIONAL: define custom key mappings, defaults are: "key_map": { diff --git a/config/config.go b/config/config.go index 8e4e33e..01cefaf 100644 --- a/config/config.go +++ b/config/config.go @@ -12,6 +12,7 @@ import ( // Config is the definition of a Config struct type Config struct { SlackToken string `json:"slack_token"` + Notify bool `json:"notify"` SidebarWidth int `json:"sidebar_width"` MainWidth int `json:"-"` KeyMap map[string]keyMapping `json:"key_map"` @@ -59,6 +60,7 @@ func getDefaultConfig() Config { return Config{ SidebarWidth: 1, MainWidth: 11, + Notify: true, KeyMap: map[string]keyMapping{ "command": { "i": "mode-insert", diff --git a/context/context.go b/context/context.go index 16f871d..3d75b64 100644 --- a/context/context.go +++ b/context/context.go @@ -4,6 +4,7 @@ import ( "net/http" _ "net/http/pprof" + "github.com/0xAX/notificator" "github.com/erroneousboat/termui" termbox "github.com/nsf/termbox-go" @@ -26,6 +27,7 @@ type AppContext struct { Config *config.Config Debug bool Mode string + Notify *notificator.Notificator } // CreateAppContext creates an application context which can be passed @@ -92,5 +94,6 @@ func CreateAppContext(flgConfig string, flgDebug bool) (*AppContext, error) { Config: config, Debug: flgDebug, Mode: CommandMode, + Notify: notificator.New(notificator.Options{AppName: "slack-term"}), }, nil } diff --git a/handlers/event.go b/handlers/event.go index 172539e..ba6b43d 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "github.com/0xAX/notificator" "github.com/erroneousboat/termui" "github.com/nlopes/slack" termbox "github.com/nsf/termbox-go" @@ -14,7 +15,8 @@ import ( "github.com/erroneousboat/slack-term/views" ) -var timer *time.Timer +var scrollTimer *time.Timer +var notifyTimer *time.Timer // actionMap binds specific action names to the function counterparts, // these action names can then be used to bind them to specific keys @@ -114,7 +116,7 @@ func messageHandler(ctx *context.AppContext) { // Add message to the selected channel if ev.Channel == ctx.Service.Channels[ctx.View.Channels.SelectedChannel].ID { - // reverse order of messages, mainly done + // Reverse order of messages, mainly done // when attachments are added to message for i := len(msg) - 1; i >= 0; i-- { ctx.View.Chat.AddMessage( @@ -252,12 +254,12 @@ func actionSearch(ctx *context.AppContext, key rune) { actionInput(ctx.View, key) go func() { - if timer != nil { - timer.Stop() + if scrollTimer != nil { + scrollTimer.Stop() } - timer = time.NewTimer(time.Second / 4) - <-timer.C + scrollTimer = time.NewTimer(time.Second / 4) + <-scrollTimer.C // Only actually search when the time expires term := ctx.View.Input.GetText() @@ -311,15 +313,15 @@ func actionGetMessages(ctx *context.AppContext) { // the list without executing the actionChangeChannel event func actionMoveCursorUpChannels(ctx *context.AppContext) { go func() { - if timer != nil { - timer.Stop() + if scrollTimer != nil { + scrollTimer.Stop() } ctx.View.Channels.MoveCursorUp() termui.Render(ctx.View.Channels) - timer = time.NewTimer(time.Second / 4) - <-timer.C + scrollTimer = time.NewTimer(time.Second / 4) + <-scrollTimer.C // Only actually change channel when the timer expires actionChangeChannel(ctx) @@ -331,15 +333,15 @@ func actionMoveCursorUpChannels(ctx *context.AppContext) { // the list without executing the actionChangeChannel event func actionMoveCursorDownChannels(ctx *context.AppContext) { go func() { - if timer != nil { - timer.Stop() + if scrollTimer != nil { + scrollTimer.Stop() } ctx.View.Channels.MoveCursorDown() termui.Render(ctx.View.Channels) - timer = time.NewTimer(time.Second / 4) - <-timer.C + scrollTimer = time.NewTimer(time.Second / 4) + <-scrollTimer.C // Only actually change channel when the timer expires actionChangeChannel(ctx) @@ -402,7 +404,28 @@ func actionNewMessage(ctx *context.AppContext, channelID string) { ctx.Service.MarkAsUnread(channelID) ctx.View.Channels.SetChannels(ctx.Service.ChannelsToString()) termui.Render(ctx.View.Channels) + + // Terminal bell fmt.Print("\a") + + // Desktop notification + if ctx.Config.Notify { + go func() { + if notifyTimer != nil { + notifyTimer.Stop() + } + + notifyTimer = time.NewTimer(time.Second * 2) + <-notifyTimer.C + + // Only actually notify when time expires + ctx.Notify.Push( + "slack-term", + ctx.Service.CreateNotifyMessage(channelID), "", + notificator.UR_NORMAL, + ) + }() + } } func actionSetPresence(ctx *context.AppContext, channelID string, presence string) { diff --git a/service/slack.go b/service/slack.go index 67f54a3..97d2ffa 100644 --- a/service/slack.go +++ b/service/slack.go @@ -306,8 +306,9 @@ func (s *SlackService) MarkAsRead(channelID int) { } } -// MarkAsUnread will set the channel as unread -func (s *SlackService) MarkAsUnread(channelID string) { +// FindChannel will loop over s.Channels to find the index where the +// channelID equals the ID +func (s *SlackService) FindChannel(channelID string) int { var index int for i, channel := range s.Channels { if channel.ID == channelID { @@ -315,9 +316,21 @@ func (s *SlackService) MarkAsUnread(channelID string) { break } } + return index +} + +// MarkAsUnread will set the channel as unread +func (s *SlackService) MarkAsUnread(channelID string) { + index := s.FindChannel(channelID) s.Channels[index].Notification = true } +// GetChannelName will return the name for a specific channelID +func (s *SlackService) GetChannelName(channelID string) string { + index := s.FindChannel(channelID) + return s.Channels[index].Name +} + // SendMessage will send a message to a particular channel func (s *SlackService) SendMessage(channelID int, message string) { @@ -519,6 +532,21 @@ func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent return msgs, nil } +func (s *SlackService) CreateNotifyMessage(channelID string) string { + channel := s.Channels[s.FindChannel(channelID)] + + switch channel.Type { + case ChannelTypeChannel: + return fmt.Sprintf("Message received on channel: %s", channel.Name) + case ChannelTypeGroup: + return fmt.Sprintf("Message received in group: %s", channel.Name) + case ChannelTypeIM: + return fmt.Sprintf("Message received from: %s", channel.Name) + } + + return "" +} + // parseMessage will parse a message string and find and replace: // - emoji's // - mentions diff --git a/vendor/github.com/0xAX/notificator/.gitignore b/vendor/github.com/0xAX/notificator/.gitignore new file mode 100644 index 0000000..79d89ab --- /dev/null +++ b/vendor/github.com/0xAX/notificator/.gitignore @@ -0,0 +1,25 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test + +.idea diff --git a/vendor/github.com/0xAX/notificator/LICENSE b/vendor/github.com/0xAX/notificator/LICENSE new file mode 100644 index 0000000..015ead8 --- /dev/null +++ b/vendor/github.com/0xAX/notificator/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2014, 0xAX +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/0xAX/notificator/README.md b/vendor/github.com/0xAX/notificator/README.md new file mode 100644 index 0000000..9e71f7a --- /dev/null +++ b/vendor/github.com/0xAX/notificator/README.md @@ -0,0 +1,49 @@ +notificator +=========================== + +Desktop notification with Golang for: + + * Windows with `growlnotify`; + * Mac OS X with `terminal-notifier` (if installed) or `osascript` (native, 10.9 Mavericks or Up.); + * Linux with `notify-send` for Gnome and `kdialog` for Kde. + +Usage +------ + +```go +package main + +import ( + "github.com/0xAX/notificator" +) + +var notify *notificator.Notificator + +func main() { + + notify = notificator.New(notificator.Options{ + DefaultIcon: "icon/default.png", + AppName: "My test App", + }) + + notify.Push("title", "text", "/home/user/icon.png", notificator.UR_CRITICAL) +} +``` + +TODO +----- + + * Add more options for different notificators. + +Сontribution +------------ + + * Fork; + * Make changes; + * Send pull request; + * Thank you. + +author +---------- + +[@0xAX](https://twitter.com/0xAX) diff --git a/vendor/github.com/0xAX/notificator/notification.go b/vendor/github.com/0xAX/notificator/notification.go new file mode 100644 index 0000000..918605a --- /dev/null +++ b/vendor/github.com/0xAX/notificator/notification.go @@ -0,0 +1,166 @@ +package notificator + +import ( + "fmt" + "os/exec" + "runtime" + "strconv" + "strings" +) + +type Options struct { + DefaultIcon string + AppName string +} + +const ( + UR_NORMAL = "normal" + UR_CRITICAL = "critical" +) + +type notifier interface { + push(title string, text string, iconPath string) *exec.Cmd + pushCritical(title string, text string, iconPath string) *exec.Cmd +} + +type Notificator struct { + notifier notifier + defaultIcon string +} + +func (n Notificator) Push(title string, text string, iconPath string, urgency string) error { + icon := n.defaultIcon + + if iconPath != "" { + icon = iconPath + } + + if urgency == UR_CRITICAL { + return n.notifier.pushCritical(title, text, icon).Run() + } + + return n.notifier.push(title, text, icon).Run() + +} + +type osxNotificator struct { + AppName string +} + +func (o osxNotificator) push(title string, text string, iconPath string) *exec.Cmd { + + // Checks if terminal-notifier exists, and is accessible. + + term_notif := CheckTermNotif() + os_version_check := CheckMacOSVersion() + + // if terminal-notifier exists, use it. + // else, fall back to osascript. (Mavericks and later.) + + if term_notif == true { + return exec.Command("terminal-notifier", "-title", o.AppName, "-message", text, "-subtitle", title, "-appIcon", iconPath) + } else if os_version_check == true { + title = strings.Replace(title, `"`, `\"`, -1) + text = strings.Replace(text, `"`, `\"`, -1) + + notification := fmt.Sprintf("display notification \"%s\" with title \"%s\" subtitle \"%s\"", text, o.AppName, title) + return exec.Command("osascript", "-e", notification) + } + + // finally falls back to growlnotify. + + return exec.Command("growlnotify", "-n", o.AppName, "--image", iconPath, "-m", title) +} + +// Causes the notification to stick around until clicked. +func (o osxNotificator) pushCritical(title string, text string, iconPath string) *exec.Cmd { + + // same function as above... + + term_notif := CheckTermNotif() + os_version_check := CheckMacOSVersion() + + if term_notif == true { + // timeout set to 30 seconds, to show the importance of the notification + return exec.Command("terminal-notifier", "-title", o.AppName, "-message", text, "-subtitle", title, "-timeout", "30") + } else if os_version_check == true { + notification := fmt.Sprintf("display notification \"%s\" with title \"%s\" subtitle \"%s\"", text, o.AppName, title) + return exec.Command("osascript", "-e", notification) + } + + return exec.Command("growlnotify", "-n", o.AppName, "--image", iconPath, "-m", title) + +} + +type linuxNotificator struct{} + +func (l linuxNotificator) push(title string, text string, iconPath string) *exec.Cmd { + return exec.Command("notify-send", "-i", iconPath, title, text) +} + +// Causes the notification to stick around until clicked. +func (l linuxNotificator) pushCritical(title string, text string, iconPath string) *exec.Cmd { + return exec.Command("notify-send", "-i", iconPath, title, text, "-u", "critical") +} + +type windowsNotificator struct{} + +func (w windowsNotificator) push(title string, text string, iconPath string) *exec.Cmd { + return exec.Command("growlnotify", "/i:", iconPath, "/t:", title, text) +} + +// Causes the notification to stick around until clicked. +func (w windowsNotificator) pushCritical(title string, text string, iconPath string) *exec.Cmd { + return exec.Command("notify-send", "-i", iconPath, title, text, "/s", "true", "/p", "2") +} + +func New(o Options) *Notificator { + + var Notifier notifier + + switch runtime.GOOS { + + case "darwin": + Notifier = osxNotificator{AppName: o.AppName} + case "linux": + Notifier = linuxNotificator{} + case "windows": + Notifier = windowsNotificator{} + + } + + return &Notificator{notifier: Notifier, defaultIcon: o.DefaultIcon} +} + +// Helper function for macOS + +func CheckTermNotif() bool { + // Checks if terminal-notifier exists, and is accessible. + if err := exec.Command("which", "terminal-notifier").Run(); err != nil { + return false + } + // no error, so return true. (terminal-notifier exists) + return true +} + +func CheckMacOSVersion() bool { + // Checks if the version of macOS is 10.9 or Higher (osascript support for notifications.) + + cmd := exec.Command("sw_vers", "-productVersion") + check, _ := cmd.Output() + + version := strings.Split(strings.TrimSpace(string(check)), ".") + + // semantic versioning of macOS + + major, _ := strconv.Atoi(version[0]) + minor, _ := strconv.Atoi(version[1]) + + if major < 10 { + return false + } else if major == 10 && minor < 9 { + return false + } else { + return true + } +} From 56000c4f3ecdbdbc375ccc6c77b390bf2222d975 Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Fri, 6 Apr 2018 13:42:12 +0200 Subject: [PATCH 10/12] Implement configuration for desktop notifications --- README.md | 7 ++++--- config/config.go | 16 ++++++++++++++-- handlers/event.go | 49 +++++++++++++++++++++++++++++------------------ service/slack.go | 23 ++++++++++++++++++++++ 4 files changed, 71 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index bf58487..3c02c46 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,10 @@ Setup // OPTIONAL: set the width of the sidebar (between 1 and 11), default is 1 "sidebar_width": 1, - // OPTIONAL: turn on desktop notifications for all incoming messages, - default is false - "notify": false, + // OPTIONAL: turn on desktop notifications for all incoming messages, set + // the value as: "all", and for only mentions and im messages set the + // value as: "mention", default is turned off: "" + "notify": "", // OPTIONAL: define custom key mappings, defaults are: "key_map": { diff --git a/config/config.go b/config/config.go index 01cefaf..a4d190a 100644 --- a/config/config.go +++ b/config/config.go @@ -9,10 +9,15 @@ import ( "github.com/erroneousboat/termui" ) +const ( + NotifyAll = "all" + NotifyMention = "mention" +) + // Config is the definition of a Config struct type Config struct { SlackToken string `json:"slack_token"` - Notify bool `json:"notify"` + Notify string `json:"notify"` SidebarWidth int `json:"sidebar_width"` MainWidth int `json:"-"` KeyMap map[string]keyMapping `json:"key_map"` @@ -44,6 +49,13 @@ func NewConfig(filepath string) (*Config, error) { cfg.MainWidth = 12 - cfg.SidebarWidth + switch cfg.Notify { + case NotifyAll, NotifyMention, "": + break + default: + return &cfg, fmt.Errorf("unsupported setting for notify: %s", cfg.Notify) + } + termui.ColorMap = map[string]termui.Attribute{ "fg": termui.StringToAttribute(cfg.Theme.View.Fg), "bg": termui.StringToAttribute(cfg.Theme.View.Bg), @@ -60,7 +72,7 @@ func getDefaultConfig() Config { return Config{ SidebarWidth: 1, MainWidth: 11, - Notify: true, + Notify: "", KeyMap: map[string]keyMapping{ "command": { "i": "mode-insert", diff --git a/handlers/event.go b/handlers/event.go index ba6b43d..4d5473f 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -11,6 +11,7 @@ import ( "github.com/nlopes/slack" termbox "github.com/nsf/termbox-go" + "github.com/erroneousboat/slack-term/config" "github.com/erroneousboat/slack-term/context" "github.com/erroneousboat/slack-term/views" ) @@ -136,7 +137,7 @@ func messageHandler(ctx *context.AppContext) { // window (tmux). But only create a notification when // it comes from someone else but the current user. if ev.User != ctx.Service.CurrentUserID { - actionNewMessage(ctx, ev.Channel) + actionNewMessage(ctx, ev) } case *slack.PresenceChangeEvent: actionSetPresence(ctx, ev.User, ev.Presence) @@ -400,8 +401,10 @@ func actionChangeChannel(ctx *context.AppContext) { termui.Render(ctx.View.Chat) } -func actionNewMessage(ctx *context.AppContext, channelID string) { - ctx.Service.MarkAsUnread(channelID) +// actionNewMessage will set the new message indicator for a channel, and +// if configured will also display a desktop notification +func actionNewMessage(ctx *context.AppContext, ev *slack.MessageEvent) { + ctx.Service.MarkAsUnread(ev.Channel) ctx.View.Channels.SetChannels(ctx.Service.ChannelsToString()) termui.Render(ctx.View.Channels) @@ -409,22 +412,12 @@ func actionNewMessage(ctx *context.AppContext, channelID string) { fmt.Print("\a") // Desktop notification - if ctx.Config.Notify { - go func() { - if notifyTimer != nil { - notifyTimer.Stop() - } - - notifyTimer = time.NewTimer(time.Second * 2) - <-notifyTimer.C - - // Only actually notify when time expires - ctx.Notify.Push( - "slack-term", - ctx.Service.CreateNotifyMessage(channelID), "", - notificator.UR_NORMAL, - ) - }() + if ctx.Config.Notify == config.NotifyMention { + if ctx.Service.CheckNotifyMention(ev) { + createNotifyMessage(ctx, ev) + } + } else if ctx.Config.Notify == config.NotifyAll { + createNotifyMessage(ctx, ev) } } @@ -498,3 +491,21 @@ func getKeyString(e termbox.Event) string { ek = pre + mod + k return ek } + +func createNotifyMessage(ctx *context.AppContext, ev *slack.MessageEvent) { + go func() { + if notifyTimer != nil { + notifyTimer.Stop() + } + + notifyTimer = time.NewTimer(time.Second * 2) + <-notifyTimer.C + + // Only actually notify when time expires + ctx.Notify.Push( + "slack-term", + ctx.Service.CreateNotifyMessage(ev.Channel), "", + notificator.UR_NORMAL, + ) + }() +} diff --git a/service/slack.go b/service/slack.go index 97d2ffa..8ef1d53 100644 --- a/service/slack.go +++ b/service/slack.go @@ -532,6 +532,29 @@ func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent return msgs, nil } +// CheckNotifyMention check if the message event is either contains a +// mention or is posted on an IM channel +func (s *SlackService) CheckNotifyMention(ev *slack.MessageEvent) bool { + channel := s.Channels[s.FindChannel(ev.Channel)] + switch channel.Type { + case ChannelTypeIM: + return true + } + + // Mentions have the following format: + // <@U12345|erroneousboat> + // <@U12345> + r := regexp.MustCompile(`\<@(\w+\|*\w+)\>`) + matches := r.FindAllString(ev.Text, -1) + for _, match := range matches { + if strings.Contains(match, s.CurrentUserID) { + return true + } + } + + return false +} + func (s *SlackService) CreateNotifyMessage(channelID string) string { channel := s.Channels[s.FindChannel(channelID)] From a98409901d9718f63ff76fdbd50336bc770d6d52 Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Sat, 7 Apr 2018 11:17:25 +0200 Subject: [PATCH 11/12] Set slack token as flag or env variable Fixes #133 --- config/config.go | 4 ---- context/context.go | 13 ++++++++++++- main.go | 10 +++++++++- service/slack.go | 2 +- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/config/config.go b/config/config.go index a4d190a..abde6e4 100644 --- a/config/config.go +++ b/config/config.go @@ -39,10 +39,6 @@ func NewConfig(filepath string) (*Config, error) { return &cfg, fmt.Errorf("the slack-term config file isn't valid json: %v", err) } - if cfg.SlackToken == "" { - return &cfg, errors.New("couldn't find 'slack_token' parameter") - } - if cfg.SidebarWidth < 1 || cfg.SidebarWidth > 11 { return &cfg, errors.New("please specify the 'sidebar_width' between 1 and 11") } diff --git a/context/context.go b/context/context.go index 3d75b64..ffad4f9 100644 --- a/context/context.go +++ b/context/context.go @@ -3,6 +3,7 @@ package context import ( "net/http" _ "net/http/pprof" + "os" "github.com/0xAX/notificator" "github.com/erroneousboat/termui" @@ -32,7 +33,7 @@ type AppContext struct { // CreateAppContext creates an application context which can be passed // and referenced througout the application -func CreateAppContext(flgConfig string, flgDebug bool) (*AppContext, error) { +func CreateAppContext(flgConfig string, flgToken string, flgDebug bool) (*AppContext, error) { if flgDebug { go func() { http.ListenAndServe(":6060", nil) @@ -48,6 +49,16 @@ func CreateAppContext(flgConfig string, flgDebug bool) (*AppContext, error) { return nil, err } + // When slack token isn't set in the config file, we'll check + // the command-line flag or the environment variable + if config.SlackToken == "" { + if flgToken != "" { + config.SlackToken = flgToken + } else { + config.SlackToken = os.Getenv("SLACK_TOKEN") + } + } + // Create Service svc, err := service.NewSlackService(config) if err != nil { diff --git a/main.go b/main.go index 61f5588..23515b1 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,7 @@ GLOBAL OPTIONS: var ( flgConfig string + flgToken string flgDebug bool flgUsage bool ) @@ -55,6 +56,13 @@ func init() { "location of config file", ) + flag.StringVar( + &flgToken, + "token", + "", + "the slack token", + ) + flag.BoolVar( &flgDebug, "debug", @@ -87,7 +95,7 @@ func main() { termui.DefaultEvtStream = customEvtStream // Create context - ctx, err := context.CreateAppContext(flgConfig, flgDebug) + ctx, err := context.CreateAppContext(flgConfig, flgToken, flgDebug) if err != nil { termbox.Close() log.Println(err) diff --git a/service/slack.go b/service/slack.go index 8ef1d53..7ef28c2 100644 --- a/service/slack.go +++ b/service/slack.go @@ -47,7 +47,7 @@ func NewSlackService(config *config.Config) (*SlackService, error) { // arrives authTest, err := svc.Client.AuthTest() if err != nil { - return nil, errors.New("not able to authorize client, check your connection and or slack-token") + return nil, errors.New("not able to authorize client, check your connection and if your slack-token is set correctly") } svc.CurrentUserID = authTest.UserID From b8b2c42f21c581f4ba97faf03fd3a7f596bcd029 Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Sat, 14 Apr 2018 17:48:32 +0200 Subject: [PATCH 12/12] Update version to 0.4.0 --- README.md | 4 +++- main.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3c02c46..6a7ca76 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,12 @@ $ mv slack-term /usr/local/bin #### Via Go -If you want you can also get `slack-term` via Go: +If you want, you can also get `slack-term` via Go: ```bash $ go get -u github.com/erroneousboat/slack-term +$ cd $GOPATH/src/github.com/erroneousboat/slack-term +$ go install . ``` Setup diff --git a/main.go b/main.go index 23515b1..3784098 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( ) const ( - VERSION = "v0.3.2" + VERSION = "v0.4.0" USAGE = `NAME: slack-term - slack client for your terminal