diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..c55509f --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,49 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + branch = "master" + digest = "1:226f155d82a49a4ede16d75f32ae4ef28493be444e37e151da6421d2292c8d6d" + name = "github.com/dpb587/go-pairist" + packages = [ + "api", + "denormalized", + ] + pruneopts = "UT" + revision = "176555c511e261941cb4d4fdd1740c7c16d94f12" + +[[projects]] + digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d" + name = "github.com/gorilla/websocket" + packages = ["."] + pruneopts = "UT" + revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d" + version = "v1.4.0" + +[[projects]] + digest = "1:ace662a36243b5cdc2f71e654175dc192f903fafbf3411a95bc910c1cad53ce7" + name = "github.com/nlopes/slack" + packages = ["."] + pruneopts = "UT" + revision = "0db1d5eae1116bf7c8ed96c6749acfbf4daaec3e" + version = "v0.3.0" + +[[projects]] + digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747" + name = "github.com/pkg/errors" + packages = ["."] + pruneopts = "UT" + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + input-imports = [ + "github.com/dpb587/go-pairist/api", + "github.com/dpb587/go-pairist/denormalized", + "github.com/nlopes/slack", + "github.com/pkg/errors", + ] + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..e5ce7fe --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,42 @@ +# 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]] + branch = "master" + name = "github.com/dpb587/go-pairist" + +[[constraint]] + name = "github.com/nlopes/slack" + version = "0.3.0" + +[[constraint]] + name = "github.com/pkg/errors" + version = "0.8.0" + +[prune] + go-tests = true + unused-packages = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..48268a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +Copyright (c) 2017 Danny Berger + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..65cf6d6 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# go-slack-topic-bot + +Some utilities for making a [Slack](https://slack.com/) bot that manages a channel's topic. + + +## License + +[MIT License](LICENSE) diff --git a/main/cfbosh.go b/main/cfbosh.go new file mode 100644 index 0000000..00de6b3 --- /dev/null +++ b/main/cfbosh.go @@ -0,0 +1,60 @@ +package main + +import ( + "log" + "os" + + "github.com/dpb587/go-slack-topic-bot/message" + "github.com/dpb587/go-slack-topic-bot/message/boshio" + "github.com/dpb587/go-slack-topic-bot/message/github" + "github.com/dpb587/go-slack-topic-bot/message/pairist" + "github.com/dpb587/go-slack-topic-bot/slack" +) + +func main() { + msg, err := message.Join( + " || ", + message.Prefix( + "_interrupt_ ", + message.Join( + " ", + pairist.Interrupt{ + Team: "sf-bosh", + Interruptible: pairist.InterruptibleHours("12:00", "18:00", "America/Los_Angeles"), + People: map[string]string{ + "Luan": "luan", + "Josh R": "jrussett", + "Josh": "jaresty", + "Danny": "dberger", + "Mike": "mxu", + "Jim": "jfmyers9", + "Morgan": "mfine", + "Belinda": "belinda_liu", + "Max": "mpetersen", + }, + }, + ), + ), + message.Literal("_docs_ "), + message.Prefix( + "_latest_ ", + message.Join( + " ", + boshio.Release{Alias: "bosh", Repository: "github.com/cloudfoundry/bosh"}, + github.Release{Alias: "bosh-cli", Owner: "cloudfoundry", Name: "bosh-cli"}, + boshio.Stemcell{Alias: "ubuntu-xenial", Name: "bosh-aws-xen-hvm-ubuntu-xenial-go_agent"}, + boshio.Stemcell{Alias: "ubuntu-trusty", Name: "bosh-aws-xen-hvm-ubuntu-trusty-go_agent"}, + ), + ), + ).Message() + if err != nil { + log.Panicf("ERROR: %v", err) + } + + log.Printf("DEBUG: expected message: %s", msg) + + err = slack.UpdateChannelTopic(os.Getenv("SLACK_CHANNEL"), msg) + if err != nil { + log.Panicf("ERROR: %v", err) + } +} diff --git a/message/boshio/release.go b/message/boshio/release.go new file mode 100644 index 0000000..6e84eae --- /dev/null +++ b/message/boshio/release.go @@ -0,0 +1,47 @@ +package boshio + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/pkg/errors" + "github.com/dpb587/go-slack-topic-bot/message" +) + +type Release struct { + Alias string + Repository string +} + +var _ message.Messager = &Release{} + +type boshioReleaseApiV1 []struct { + Version string `json:"version"` +} + +func (m Release) Message() (string, error) { + res, err := http.DefaultClient.Get(fmt.Sprintf("https://bosh.io/api/v1/releases/%s", m.Repository)) + if err != nil { + return "", err + } + + resBodyBytes, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", errors.Wrap(err, "reading response") + } + + var data boshioReleaseApiV1 + + err = json.Unmarshal(resBodyBytes, &data) + if err != nil { + return "", errors.Wrap(err, "unmarshalling") + } + + if len(data) == 0 { + return "", nil + } + + return fmt.Sprintf("%s/%s", m.Alias, data[0].Version), nil +} diff --git a/message/boshio/stemcell.go b/message/boshio/stemcell.go new file mode 100644 index 0000000..53ad217 --- /dev/null +++ b/message/boshio/stemcell.go @@ -0,0 +1,47 @@ +package boshio + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/pkg/errors" + "github.com/dpb587/go-slack-topic-bot/message" +) + +type Stemcell struct { + Alias string + Name string +} + +var _ message.Messager = &Stemcell{} + +type boshioStemcellApiV1 []struct { + Version string `json:"version"` +} + +func (m Stemcell) Message() (string, error) { + res, err := http.DefaultClient.Get(fmt.Sprintf("https://bosh.io/api/v1/stemcells/%s", m.Name)) + if err != nil { + return "", err + } + + resBodyBytes, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", errors.Wrap(err, "reading response") + } + + var data boshioStemcellApiV1 + + err = json.Unmarshal(resBodyBytes, &data) + if err != nil { + return "", errors.Wrap(err, "unmarshalling") + } + + if len(data) == 0 { + return "", nil + } + + return fmt.Sprintf("%s/%s", m.Alias, data[0].Version), nil +} diff --git a/message/github/release.go b/message/github/release.go new file mode 100644 index 0000000..f37299c --- /dev/null +++ b/message/github/release.go @@ -0,0 +1,48 @@ +package github + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/pkg/errors" + "github.com/dpb587/go-slack-topic-bot/message" +) + +type Release struct { + Alias string + Owner string + Name string +} + +var _ message.Messager = &Release{} + +type gitHubReleaseApiV2 []struct { + Name string `json:"name"` +} + +func (m Release) Message() (string, error) { + res, err := http.DefaultClient.Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", m.Owner, m.Name)) + if err != nil { + return "", err + } + + resBodyBytes, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", errors.Wrap(err, "reading response") + } + + var data gitHubReleaseApiV2 + + err = json.Unmarshal(resBodyBytes, &data) + if err != nil { + return "", errors.Wrap(err, "unmarshalling") + } + + if len(data) == 0 { + return "", nil + } + + return fmt.Sprintf("%s/%s", m.Alias, data[0].Name), nil +} diff --git a/message/joiner.go b/message/joiner.go new file mode 100644 index 0000000..18f0585 --- /dev/null +++ b/message/joiner.go @@ -0,0 +1,40 @@ +package message + +import ( + "strings" + + "github.com/pkg/errors" +) + +type Joiner struct { + delimiter string + messages []Messager +} + +var _ Messager = &Joiner{} + +func Join(delimiter string, templates ...Messager) Messager { + return Joiner{ + delimiter: delimiter, + messages: templates, + } +} + +func (m Joiner) Message() (string, error) { + var msgs []string + + for tplIdx, tpl := range m.messages { + msg, err := tpl.Message() + if err != nil { + return "", errors.Wrapf(err, "template %d", tplIdx) + } + + if msg == "" { + continue + } + + msgs = append(msgs, msg) + } + + return strings.Join(msgs, m.delimiter), nil +} diff --git a/message/literal.go b/message/literal.go new file mode 100644 index 0000000..54a4a9c --- /dev/null +++ b/message/literal.go @@ -0,0 +1,17 @@ +package message + +type literal struct { + message string +} + +func Literal(message string) Messager { + return &literal{ + message: message, + } +} + +var _ Messager = &literal{} + +func (m literal) Message() (string, error) { + return m.message, nil +} diff --git a/message/messager.go b/message/messager.go new file mode 100644 index 0000000..4a6a971 --- /dev/null +++ b/message/messager.go @@ -0,0 +1,5 @@ +package message + +type Messager interface { + Message() (string, error) +} diff --git a/message/pairist/interrupt.go b/message/pairist/interrupt.go new file mode 100644 index 0000000..9e232ee --- /dev/null +++ b/message/pairist/interrupt.go @@ -0,0 +1,72 @@ +package pairist + +import ( + "fmt" + "sort" + "strings" + "time" + + "github.com/dpb587/go-pairist/api" + "github.com/dpb587/go-pairist/denormalized" + "github.com/dpb587/go-slack-topic-bot/message" +) + +type Interrupt struct { + Team string + People map[string]string + Interruptible func() bool +} + +var _ message.Messager = &Interrupt{} + +func (m Interrupt) Message() (string, error) { + if m.Interruptible != nil && !m.Interruptible() { + return "", nil + } + + curr, err := api.DefaultClient.GetTeamCurrent(m.Team) + if err != nil { + return "", err + } + + var handles []string + + for _, lane := range denormalized.BuildLanes(curr).ByRole("interrupt") { + for _, person := range lane.People { + if handle, ok := m.People[person.Name]; ok { + handles = append(handles, fmt.Sprintf("@%s", handle)) + } else { + handles = append(handles, person.Name) + } + } + } + + if len(handles) == 0 { + return "", nil + } + + sort.Strings(handles) + + return strings.Join(handles, " "), nil +} + +func InterruptibleHours(start, stop, tzName string) func() bool { + tz, err := time.LoadLocation(tzName) + if err != nil { + panic(err) + } + + return func() bool { + now := time.Now().In(tz) + + dt := now.Format("Mon") + + if dt == "Sat" || dt == "Sun" { + return false + } + + ts := now.Format("15:04") + + return ts >= start && ts < stop + } +} diff --git a/message/prefixer.go b/message/prefixer.go new file mode 100644 index 0000000..0ad67ef --- /dev/null +++ b/message/prefixer.go @@ -0,0 +1,32 @@ +package message + +import ( + "fmt" +) + +type Prefixer struct { + prefix string + message Messager +} + +func Prefix(prefix string, message Messager) Messager { + return Prefixer{ + prefix: prefix, + message: message, + } +} + +var _ Messager = &Prefixer{} + +func (m Prefixer) Message() (string, error) { + msg, err := m.message.Message() + if err != nil { + return "", err + } + + if msg == "" { + return "", nil + } + + return fmt.Sprintf("%s%s", m.prefix, msg), nil +} diff --git a/slack/util.go b/slack/util.go new file mode 100644 index 0000000..8997668 --- /dev/null +++ b/slack/util.go @@ -0,0 +1,35 @@ +package slack + +import ( + "log" + "os" + + "github.com/nlopes/slack" + "github.com/pkg/errors" +) + +func UpdateChannelTopic(channel, msg string) error { + api := slack.New(os.Getenv("SLACK_TOKEN")) + + channelInfo, err := api.GetChannelInfo(channel) + if err != nil { + return errors.Wrap(err, "getting channel info") + } + + log.Printf("DEBUG: current channel topic: %s", channelInfo.Topic.Value) + + if channelInfo.Topic.Value == msg { + log.Printf("DEBUG: channel topic already set") + + return nil + } + + newTopic, err := api.SetChannelTopic(channel, msg) + if err != nil { + return errors.Wrap(err, "setting topic") + } + + log.Printf("INFO: channel topic updated: %s", newTopic) + + return nil +} diff --git a/vendor/github.com/dpb587/go-pairist/LICENSE b/vendor/github.com/dpb587/go-pairist/LICENSE new file mode 100644 index 0000000..c425abd --- /dev/null +++ b/vendor/github.com/dpb587/go-pairist/LICENSE @@ -0,0 +1,18 @@ +Copyright (c) 2018 Danny Berger + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/dpb587/go-pairist/api/client.go b/vendor/github.com/dpb587/go-pairist/api/client.go new file mode 100644 index 0000000..147de14 --- /dev/null +++ b/vendor/github.com/dpb587/go-pairist/api/client.go @@ -0,0 +1,43 @@ +package api + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/pkg/errors" +) + +var DefaultClient = NewClient(http.DefaultClient, "https://pairist-9de4d.firebaseio.com") + +type Client struct { + client *http.Client + baseURL string +} + +func NewClient(client *http.Client, baseURL string) *Client { + return &Client{ + client: client, + baseURL: baseURL, + } +} + +func (c *Client) Get(path string, data interface{}) error { + res, err := c.client.Get(fmt.Sprintf("%s/%s", c.baseURL, path)) + if err != nil { + return err + } + + bytes, err := ioutil.ReadAll(res.Body) + if err != nil { + return errors.Wrap(err, "reading response body") + } + + err = json.Unmarshal(bytes, data) + if err != nil { + return errors.Wrap(err, "unmarshalling response") + } + + return nil +} diff --git a/vendor/github.com/dpb587/go-pairist/api/team_history.go b/vendor/github.com/dpb587/go-pairist/api/team_history.go new file mode 100644 index 0000000..536b595 --- /dev/null +++ b/vendor/github.com/dpb587/go-pairist/api/team_history.go @@ -0,0 +1,37 @@ +package api + +import ( + "fmt" +) + +type TeamHistorical struct { + Entities map[string]TeamHistoricalEntity `json:"entities,omitempty"` + Lanes map[string]TeamHistoricalLane `json:"lanes,omitempty"` +} + +type TeamHistoricalEntity struct { + Color string `json:"color,omitempty"` + Icon string `json:"icon,omitempty"` + Location string `json:"location,omitempty"` + Name string `json:"name,omitempty"` + Picture string `json:"picture,omitempty"` + Tags []string `json:"tags,omitempty"` + Type string `json:"type,omitempty"` + UpdatedAt uint `json:"updatedAt,omitempty"` +} + +type TeamHistoricalLane struct { + Locked bool `json:"locked,omitempty"` + SortOrder uint `json:"sortOrder,omitempty"` +} + +func (c *Client) GetTeamCurrent(team string) (*TeamHistorical, error) { + var res TeamHistorical + + err := c.Get(fmt.Sprintf("teams/%s/current.json", team), &res) + if err != nil { + return nil, err + } + + return &res, nil +} diff --git a/vendor/github.com/dpb587/go-pairist/api/team_lists.go b/vendor/github.com/dpb587/go-pairist/api/team_lists.go new file mode 100644 index 0000000..fa7033f --- /dev/null +++ b/vendor/github.com/dpb587/go-pairist/api/team_lists.go @@ -0,0 +1,30 @@ +package api + +import ( + "fmt" +) + +type TeamLists map[string]TeamList + +type TeamList struct { + Items TeamListItems `json:"items,omitempty"` + Title string `json:"title,omitempty"` +} + +type TeamListItems map[string]TeamListItem + +type TeamListItem struct { + Checked bool `json:"checked,omitempty"` + Title string `json:"title,omitempty"` +} + +func (c *Client) GetTeamLists(team string) (*TeamLists, error) { + var res TeamLists + + err := c.Get(fmt.Sprintf("teams/%s/lists.json", team), &res) + if err != nil { + return nil, err + } + + return &res, nil +} diff --git a/vendor/github.com/dpb587/go-pairist/denormalized/lanes.go b/vendor/github.com/dpb587/go-pairist/denormalized/lanes.go new file mode 100644 index 0000000..8219f16 --- /dev/null +++ b/vendor/github.com/dpb587/go-pairist/denormalized/lanes.go @@ -0,0 +1,73 @@ +package denormalized + +import ( + "github.com/dpb587/go-pairist/api" +) + +type Lane struct { + ID string + People []Entity + Roles []Entity + Tracks []Entity +} + +type Lanes []Lane + +func (l Lanes) ByRole(name string) []Lane { + var res []Lane + + for _, r := range l { + for _, b := range r.Roles { + if b.Name == name { + res = append(res, r) + + break + } + } + } + + return res +} + +type Entity struct { + Color string + Icon string + Name string + Picture string + UpdatedAt uint +} + +func BuildLanes(historical *api.TeamHistorical) Lanes { + var lanes Lanes + + for laneID := range historical.Lanes { + lane := Lane{} + + for _, entity := range historical.Entities { + if entity.Location != laneID { + continue + } + + denormalizedEntity := Entity{ + Color: entity.Color, + Icon: entity.Icon, + Name: entity.Name, + Picture: entity.Picture, + UpdatedAt: entity.UpdatedAt, + } + + switch entity.Type { + case "person": + lane.People = append(lane.People, denormalizedEntity) + case "role": + lane.Roles = append(lane.Roles, denormalizedEntity) + case "track": + lane.Tracks = append(lane.Tracks, denormalizedEntity) + } + } + + lanes = append(lanes, lane) + } + + return lanes +} diff --git a/vendor/github.com/gorilla/websocket/.gitignore b/vendor/github.com/gorilla/websocket/.gitignore new file mode 100644 index 0000000..cd3fcd1 --- /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 diff --git a/vendor/github.com/gorilla/websocket/.travis.yml b/vendor/github.com/gorilla/websocket/.travis.yml new file mode 100644 index 0000000..a49db51 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/.travis.yml @@ -0,0 +1,19 @@ +language: go +sudo: false + +matrix: + include: + - go: 1.7.x + - go: 1.8.x + - go: 1.9.x + - go: 1.10.x + - go: 1.11.x + - go: tip + allow_failures: + - go: tip + +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d .) + - go vet $(go list ./... | grep -v /vendor/) + - go test -v -race ./... diff --git a/vendor/github.com/gorilla/websocket/AUTHORS b/vendor/github.com/gorilla/websocket/AUTHORS new file mode 100644 index 0000000..1931f40 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/AUTHORS @@ -0,0 +1,9 @@ +# This is the official list of Gorilla WebSocket authors for copyright +# purposes. +# +# Please keep the list sorted. + +Gary Burd +Google LLC (https://opensource.google.com/) +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..20e391f --- /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..2e32fd5 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/client.go @@ -0,0 +1,395 @@ +// 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 ( + "bytes" + "context" + "crypto/tls" + "errors" + "io" + "io/ioutil" + "net" + "net/http" + "net/http/httptrace" + "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) + + // NetDialContext specifies the dial function for creating TCP connections. If + // NetDialContext is nil, net.DialContext is used. + NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error) + + // Proxy specifies a function to return a proxy for a given + // Request. If the function returns a non-nil error, the + // request is aborted with the provided error. + // 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 + + // WriteBufferPool is a pool of buffers for write operations. If the value + // is not set, then write buffers are allocated to the connection for the + // lifetime of the connection. + // + // A pool is most useful when the application has a modest volume of writes + // across a large number of connections. + // + // Applications should use a single pool for each unique value of + // WriteBufferSize. + WriteBufferPool BufferPool + + // Subprotocols specifies the client's requested subprotocols. + Subprotocols []string + + // 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 +} + +// Dial creates a new client connection by calling DialContext with a background context. +func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { + return d.DialContext(context.Background(), urlStr, requestHeader) +} + +var errMalformedURL = errors.New("malformed ws or wss URL") + +func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { + 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 values. +var DefaultDialer = &Dialer{ + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: 45 * time.Second, +} + +// nilDialer is dialer to use when receiver is nil. +var nilDialer = *DefaultDialer + +// DialContext creates a new client connection. Use requestHeader to specify the +// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie). +// Use the response.Header to get the selected subprotocol +// (Sec-WebSocket-Protocol) and cookies (Set-Cookie). +// +// The context will be used in the request and in the Dialer +// +// If the WebSocket handshake fails, ErrBadHandshake is returned along with a +// non-nil *http.Response so that callers can handle redirects, authentication, +// etcetera. The response body may not contain the entire response and does not +// need to be closed by the application. +func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { + if d == nil { + d = &nilDialer + } + + challengeKey, err := generateChallengeKey() + if err != nil { + return nil, nil, err + } + + u, err := url.Parse(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, + } + req = req.WithContext(ctx) + + // 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) + case k == "Sec-Websocket-Protocol": + req.Header["Sec-WebSocket-Protocol"] = vs + default: + req.Header[k] = vs + } + } + + if d.EnableCompression { + req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"} + } + + if d.HandshakeTimeout != 0 { + var cancel func() + ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout) + defer cancel() + } + + // Get network dial function. + var netDial func(network, add string) (net.Conn, error) + + if d.NetDialContext != nil { + netDial = func(network, addr string) (net.Conn, error) { + return d.NetDialContext(ctx, network, addr) + } + } else if d.NetDial != nil { + netDial = d.NetDial + } else { + netDialer := &net.Dialer{} + netDial = func(network, addr string) (net.Conn, error) { + return netDialer.DialContext(ctx, network, addr) + } + } + + // If needed, wrap the dial function to set the connection deadline. + if deadline, ok := ctx.Deadline(); ok { + forwardDial := netDial + netDial = func(network, addr string) (net.Conn, error) { + c, err := forwardDial(network, addr) + if err != nil { + return nil, err + } + err = c.SetDeadline(deadline) + if err != nil { + c.Close() + return nil, err + } + return c, nil + } + } + + // If needed, wrap the dial function to connect through a proxy. + if d.Proxy != nil { + proxyURL, err := d.Proxy(req) + if err != nil { + return nil, nil, err + } + if proxyURL != nil { + dialer, err := proxy_FromURL(proxyURL, netDialerFunc(netDial)) + if err != nil { + return nil, nil, err + } + netDial = dialer.Dial + } + } + + hostPort, hostNoPort := hostPortNoPort(u) + trace := httptrace.ContextClientTrace(ctx) + if trace != nil && trace.GetConn != nil { + trace.GetConn(hostPort) + } + + netConn, err := netDial("tcp", hostPort) + if trace != nil && trace.GotConn != nil { + trace.GotConn(httptrace.GotConnInfo{ + Conn: netConn, + }) + } + if err != nil { + return nil, nil, err + } + + defer func() { + if netConn != nil { + netConn.Close() + } + }() + + if u.Scheme == "https" { + cfg := cloneTLSConfig(d.TLSClientConfig) + if cfg.ServerName == "" { + cfg.ServerName = hostNoPort + } + tlsConn := tls.Client(netConn, cfg) + netConn = tlsConn + + var err error + if trace != nil { + err = doHandshakeWithTrace(trace, tlsConn, cfg) + } else { + err = doHandshake(tlsConn, cfg) + } + + if err != nil { + return nil, nil, err + } + } + + conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil) + + if err := req.Write(netConn); err != nil { + return nil, nil, err + } + + if trace != nil && trace.GotFirstResponseByte != nil { + if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 { + trace.GotFirstResponseByte() + } + } + + resp, err := http.ReadResponse(conn.br, req) + if err != nil { + return nil, nil, err + } + + 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 +} + +func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error { + if err := tlsConn.Handshake(); err != nil { + return err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return err + } + } + return nil +} 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..d2a21c1 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/conn.go @@ -0,0 +1,1165 @@ +// 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 pong 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 a close message. +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) +} + +// BufferPool represents a pool of buffers. The *sync.Pool type satisfies this +// interface. The type of the value stored in a pool is not specified. +type BufferPool interface { + // Get gets a value from the pool or returns nil if the pool is empty. + Get() interface{} + // Put adds a value to the pool. + Put(interface{}) +} + +// writePoolData is the type added to the write buffer pool. This wrapper is +// used to prevent applications from peeking at and depending on the values +// added to the pool. +type writePoolData struct{ buf []byte } + +// The Conn type represents a WebSocket connection. +type Conn struct { + conn net.Conn + 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. + writePool BufferPool + writeBufSize int + writeDeadline time.Time + writer io.WriteCloser // the current writer returned to the application + isWriting bool // for best-effort concurrent write detection + + 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, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn { + + if br == nil { + if readBufferSize == 0 { + readBufferSize = defaultReadBufferSize + } else if readBufferSize < maxControlFramePayloadSize { + // must be large enough for control frame + readBufferSize = maxControlFramePayloadSize + } + br = bufio.NewReaderSize(conn, readBufferSize) + } + + if writeBufferSize <= 0 { + writeBufferSize = defaultWriteBufferSize + } + writeBufferSize += maxFrameHeaderSize + + if writeBuf == nil && writeBufferPool == nil { + writeBuf = make([]byte, writeBufferSize) + } + + mu := make(chan bool, 1) + mu <- true + c := &Conn{ + isServer: isServer, + br: br, + conn: conn, + mu: mu, + readFinal: true, + writeBuf: writeBuf, + writePool: writeBufferPool, + writeBufSize: writeBufferSize, + 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 message. +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) read(n int) ([]byte, error) { + p, err := c.br.Peek(n) + if err == io.EOF { + err = errUnexpectedEOF + } + c.br.Discard(len(p)) + return p, err +} + +func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error { + <-c.mu + defer func() { c.mu <- true }() + + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + if err != nil { + return err + } + + c.conn.SetWriteDeadline(deadline) + if len(buf1) == 0 { + _, err = c.conn.Write(buf0) + } else { + err = c.writeBufs(buf0, buf1) + } + 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() + if err != nil { + return err + } + + if c.writeBuf == nil { + wpd, ok := c.writePool.Get().(writePoolData) + if ok { + c.writeBuf = wpd.buf + } else { + c.writeBuf = make([]byte, c.writeBufSize) + } + } + return nil +} + +// NextWriter returns a writer for the next message to send. The writer's Close +// 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. +// +// All message types (TextMessage, BinaryMessage, CloseMessage, PingMessage and +// PongMessage) are supported. +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 + if c.writePool != nil { + c.writePool.Put(writePoolData{buf: c.writeBuf}) + c.writeBuf = 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 message 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 +// message back to the peer. +// +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// close messages as described in the section on Control Messages above. +// +// The connection read methods return a CloseError when a close message 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 message 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 := 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 message application data. The default +// ping handler sends a pong to the peer. +// +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// ping messages as described in the section on Control Messages 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 message application data. The default +// pong handler does nothing. +// +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// pong messages as described in the section on Control Messages 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. +// An empty message is returned for code CloseNoStatusReceived. +func FormatCloseMessage(closeCode int, text string) []byte { + if closeCode == CloseNoStatusReceived { + // Return empty message because it's illegal to send + // CloseNoStatusReceived. Return non-nil value in case application + // checks for nil. + return []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_write.go b/vendor/github.com/gorilla/websocket/conn_write.go new file mode 100644 index 0000000..a509a21 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/conn_write.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 go1.8 + +package websocket + +import "net" + +func (c *Conn) writeBufs(bufs ...[]byte) error { + b := net.Buffers(bufs) + _, err := b.WriteTo(c.conn) + return err +} diff --git a/vendor/github.com/gorilla/websocket/conn_write_legacy.go b/vendor/github.com/gorilla/websocket/conn_write_legacy.go new file mode 100644 index 0000000..37edaff --- /dev/null +++ b/vendor/github.com/gorilla/websocket/conn_write_legacy.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.8 + +package websocket + +func (c *Conn) writeBufs(bufs ...[]byte) error { + for _, buf := range bufs { + if len(buf) > 0 { + if _, err := c.conn.Write(buf); err != nil { + return err + } + } + } + return nil +} diff --git a/vendor/github.com/gorilla/websocket/doc.go b/vendor/github.com/gorilla/websocket/doc.go new file mode 100644 index 0000000..dcce1a6 --- /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 calls +// the Upgrader.Upgrade method from an HTTP request handler to get 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 { +// log.Println(err) +// return +// } +// if err := conn.WriteMessage(messageType, p); err != nil { +// log.Println(err) +// return +// } +// } +// +// 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 calling the handler function +// set with the SetCloseHandler method and by returning a *CloseError from the +// NextReader, ReadMessage or the message Read method. The default close +// handler sends a close message to the peer. +// +// Connections handle received ping messages by calling the handler function +// set with the SetPingHandler method. The default ping handler sends a pong +// message to the peer. +// +// Connections handle received pong messages by calling the handler function +// set with the SetPongHandler method. The default pong handler does nothing. +// If an application sends ping messages, then the application should set a +// pong handler to receive the corresponding pong. +// +// The control message handler functions are called from the NextReader, +// ReadMessage and message reader Read methods. The default close and ping +// handlers can block these methods for a short time when the handler writes to +// the connection. +// +// The application must read the connection to process close, ping and pong +// 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 the Origin host is +// not equal to the Host request header. +// +// The deprecated package-level Upgrade function does not perform origin +// checking. The application is responsible for checking the Origin header +// before calling the Upgrade function. +// +// 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..dc2c1f6 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/json.go @@ -0,0 +1,60 @@ +// 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 writes the JSON encoding of v as a message. +// +// Deprecated: Use c.WriteJSON instead. +func WriteJSON(c *Conn, v interface{}) error { + return c.WriteJSON(v) +} + +// WriteJSON writes the JSON encoding of v as a message. +// +// 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 reads the next JSON-encoded message from the connection and stores +// it in the value pointed to by v. +// +// 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..577fce9 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/mask.go @@ -0,0 +1,54 @@ +// 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..74ec565 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/prepared.go @@ -0,0 +1,102 @@ +// 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 + 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/proxy.go b/vendor/github.com/gorilla/websocket/proxy.go new file mode 100644 index 0000000..bf2478e --- /dev/null +++ b/vendor/github.com/gorilla/websocket/proxy.go @@ -0,0 +1,77 @@ +// 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 ( + "bufio" + "encoding/base64" + "errors" + "net" + "net/http" + "net/url" + "strings" +) + +type netDialerFunc func(network, addr string) (net.Conn, error) + +func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) { + return fn(network, addr) +} + +func init() { + proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) { + return &httpProxyDialer{proxyURL: proxyURL, fowardDial: forwardDialer.Dial}, nil + }) +} + +type httpProxyDialer struct { + proxyURL *url.URL + fowardDial func(network, addr string) (net.Conn, error) +} + +func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) { + hostPort, _ := hostPortNoPort(hpd.proxyURL) + conn, err := hpd.fowardDial(network, hostPort) + if err != nil { + return nil, err + } + + connectHeader := make(http.Header) + if user := hpd.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: addr}, + Host: addr, + Header: connectHeader, + } + + if err := connectReq.Write(conn); err != nil { + conn.Close() + return nil, err + } + + // Read response. It's OK to use and discard buffered reader here becaue + // the remote server does not speak until spoken to. + br := bufio.NewReader(conn) + resp, err := http.ReadResponse(br, connectReq) + if err != nil { + conn.Close() + return nil, err + } + + if resp.StatusCode != 200 { + conn.Close() + f := strings.SplitN(resp.Status, " ", 2) + return nil, errors.New(f[1]) + } + return conn, 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..a761824 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/server.go @@ -0,0 +1,363 @@ +// 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" + "io" + "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 + + // WriteBufferPool is a pool of buffers for write operations. If the value + // is not set, then write buffers are allocated to the connection for the + // lifetime of the connection. + // + // A pool is most useful when the application has a modest volume of writes + // across a large number of connections. + // + // Applications should use a single pool for each unique value of + // WriteBufferSize. + WriteBufferPool BufferPool + + // Subprotocols specifies the server's supported protocols in order of + // preference. If this field is not nil, then the Upgrade method negotiates a + // subprotocol by selecting the first match in this list with a protocol + // requested by the client. If there's no match, then no protocol is + // negotiated (the Sec-Websocket-Protocol header is not included in the + // handshake response). + 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, then a safe default is used: return false if the + // Origin request header is present and the origin host is not equal to + // request Host header. + // + // A CheckOrigin function should carefully validate the request origin to + // prevent cross-site request forgery. + 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 equalASCIIFold(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) { + const badHandshake = "websocket: the client is not using the websocket protocol: " + + if !tokenListContainsValue(r.Header, "Connection", "upgrade") { + return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header") + } + + if !tokenListContainsValue(r.Header, "Upgrade", "websocket") { + return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header") + } + + if r.Method != "GET" { + return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET") + } + + 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") + } + + if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok { + return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported") + } + + checkOrigin := u.CheckOrigin + if checkOrigin == nil { + checkOrigin = checkSameOrigin + } + if !checkOrigin(r) { + return u.returnError(w, r, http.StatusForbidden, "websocket: request origin not allowed by Upgrader.CheckOrigin") + } + + 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 + } + } + + 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") + } + + var br *bufio.Reader + if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 { + // Reuse hijacked buffered reader as connection reader. + br = brw.Reader + } + + buf := bufioWriterBuffer(netConn, brw.Writer) + + var writeBuf []byte + if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 { + // Reuse hijacked write buffer as connection buffer. + writeBuf = buf + } + + c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf) + c.subprotocol = subprotocol + + if compress { + c.newCompressionWriter = compressNoContextTakeover + c.newDecompressionReader = decompressNoContextTakeover + } + + // Use larger of hijacked buffer and connection write buffer for header. + p := buf + if len(c.writeBuf) > len(p) { + p = c.writeBuf + } + p = p[:0] + + p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...) + p = append(p, computeAcceptKey(challengeKey)...) + p = append(p, "\r\n"...) + 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. +// +// Deprecated: Use websocket.Upgrader instead. +// +// Upgrade does not perform origin checking. The application is responsible for +// checking the Origin header before calling Upgrade. An example implementation +// of the same origin policy check is: +// +// if req.Header.Get("Origin") != "http://"+req.Host { +// http.Error(w, "Origin not allowed", http.StatusForbidden) +// 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") +} + +// bufioReaderSize size returns the size of a bufio.Reader. +func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int { + // This code assumes that peek on a reset reader returns + // bufio.Reader.buf[:0]. + // TODO: Use bufio.Reader.Size() after Go 1.10 + br.Reset(originalReader) + if p, err := br.Peek(0); err == nil { + return cap(p) + } + return 0 +} + +// writeHook is an io.Writer that records the last slice passed to it vio +// io.Writer.Write. +type writeHook struct { + p []byte +} + +func (wh *writeHook) Write(p []byte) (int, error) { + wh.p = p + return len(p), nil +} + +// bufioWriterBuffer grabs the buffer from a bufio.Writer. +func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte { + // This code assumes that bufio.Writer.buf[:1] is passed to the + // bufio.Writer's underlying writer. + var wh writeHook + bw.Reset(&wh) + bw.WriteByte(0) + bw.Flush() + + bw.Reset(originalWriter) + + return wh.p[:cap(wh.p)] +} diff --git a/vendor/github.com/gorilla/websocket/trace.go b/vendor/github.com/gorilla/websocket/trace.go new file mode 100644 index 0000000..834f122 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/trace.go @@ -0,0 +1,19 @@ +// +build go1.8 + +package websocket + +import ( + "crypto/tls" + "net/http/httptrace" +) + +func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { + if trace.TLSHandshakeStart != nil { + trace.TLSHandshakeStart() + } + err := doHandshake(tlsConn, cfg) + if trace.TLSHandshakeDone != nil { + trace.TLSHandshakeDone(tlsConn.ConnectionState(), err) + } + return err +} diff --git a/vendor/github.com/gorilla/websocket/trace_17.go b/vendor/github.com/gorilla/websocket/trace_17.go new file mode 100644 index 0000000..77d05a0 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/trace_17.go @@ -0,0 +1,12 @@ +// +build !go1.8 + +package websocket + +import ( + "crypto/tls" + "net/http/httptrace" +) + +func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { + return doHandshake(tlsConn, cfg) +} diff --git a/vendor/github.com/gorilla/websocket/util.go b/vendor/github.com/gorilla/websocket/util.go new file mode 100644 index 0000000..354001e --- /dev/null +++ b/vendor/github.com/gorilla/websocket/util.go @@ -0,0 +1,237 @@ +// 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" + "unicode/utf8" +) + +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++ + case b == '\\': + escape = true + case b == '"': + return string(p[:j]), s[i+1:] + default: + p[j] = b + j++ + } + } + return "", "" + } + } + return "", "" +} + +// equalASCIIFold returns true if s is equal to t with ASCII case folding. +func equalASCIIFold(s, t string) bool { + for s != "" && t != "" { + sr, size := utf8.DecodeRuneInString(s) + s = s[size:] + tr, size := utf8.DecodeRuneInString(t) + t = t[size:] + if sr == tr { + continue + } + if 'A' <= sr && sr <= 'Z' { + sr = sr + 'a' - 'A' + } + if 'A' <= tr && tr <= 'Z' { + tr = tr + 'a' - 'A' + } + if sr != tr { + return false + } + } + return s == t +} + +// tokenListContainsValue returns true if the 1#token header with the given +// name contains a token equal to value with ASCII case folding. +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 equalASCIIFold(t, value) { + return true + } + if s == "" { + continue headers + } + s = s[1:] + } + } + return false +} + +// parseExtensions 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/gorilla/websocket/x_net_proxy.go b/vendor/github.com/gorilla/websocket/x_net_proxy.go new file mode 100644 index 0000000..2e668f6 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/x_net_proxy.go @@ -0,0 +1,473 @@ +// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT. +//go:generate bundle -o x_net_proxy.go golang.org/x/net/proxy + +// Package proxy provides support for a variety of protocols to proxy network +// data. +// + +package websocket + +import ( + "errors" + "io" + "net" + "net/url" + "os" + "strconv" + "strings" + "sync" +) + +type proxy_direct struct{} + +// Direct is a direct proxy: one that makes network connections directly. +var proxy_Direct = proxy_direct{} + +func (proxy_direct) Dial(network, addr string) (net.Conn, error) { + return net.Dial(network, addr) +} + +// A PerHost directs connections to a default Dialer unless the host name +// requested matches one of a number of exceptions. +type proxy_PerHost struct { + def, bypass proxy_Dialer + + bypassNetworks []*net.IPNet + bypassIPs []net.IP + bypassZones []string + bypassHosts []string +} + +// NewPerHost returns a PerHost Dialer that directs connections to either +// defaultDialer or bypass, depending on whether the connection matches one of +// the configured rules. +func proxy_NewPerHost(defaultDialer, bypass proxy_Dialer) *proxy_PerHost { + return &proxy_PerHost{ + def: defaultDialer, + bypass: bypass, + } +} + +// Dial connects to the address addr on the given network through either +// defaultDialer or bypass. +func (p *proxy_PerHost) Dial(network, addr string) (c net.Conn, err error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + return p.dialerForRequest(host).Dial(network, addr) +} + +func (p *proxy_PerHost) dialerForRequest(host string) proxy_Dialer { + if ip := net.ParseIP(host); ip != nil { + for _, net := range p.bypassNetworks { + if net.Contains(ip) { + return p.bypass + } + } + for _, bypassIP := range p.bypassIPs { + if bypassIP.Equal(ip) { + return p.bypass + } + } + return p.def + } + + for _, zone := range p.bypassZones { + if strings.HasSuffix(host, zone) { + return p.bypass + } + if host == zone[1:] { + // For a zone ".example.com", we match "example.com" + // too. + return p.bypass + } + } + for _, bypassHost := range p.bypassHosts { + if bypassHost == host { + return p.bypass + } + } + return p.def +} + +// AddFromString parses a string that contains comma-separated values +// specifying hosts that should use the bypass proxy. Each value is either an +// IP address, a CIDR range, a zone (*.example.com) or a host name +// (localhost). A best effort is made to parse the string and errors are +// ignored. +func (p *proxy_PerHost) AddFromString(s string) { + hosts := strings.Split(s, ",") + for _, host := range hosts { + host = strings.TrimSpace(host) + if len(host) == 0 { + continue + } + if strings.Contains(host, "/") { + // We assume that it's a CIDR address like 127.0.0.0/8 + if _, net, err := net.ParseCIDR(host); err == nil { + p.AddNetwork(net) + } + continue + } + if ip := net.ParseIP(host); ip != nil { + p.AddIP(ip) + continue + } + if strings.HasPrefix(host, "*.") { + p.AddZone(host[1:]) + continue + } + p.AddHost(host) + } +} + +// AddIP specifies an IP address that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match an IP. +func (p *proxy_PerHost) AddIP(ip net.IP) { + p.bypassIPs = append(p.bypassIPs, ip) +} + +// AddNetwork specifies an IP range that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match. +func (p *proxy_PerHost) AddNetwork(net *net.IPNet) { + p.bypassNetworks = append(p.bypassNetworks, net) +} + +// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of +// "example.com" matches "example.com" and all of its subdomains. +func (p *proxy_PerHost) AddZone(zone string) { + if strings.HasSuffix(zone, ".") { + zone = zone[:len(zone)-1] + } + if !strings.HasPrefix(zone, ".") { + zone = "." + zone + } + p.bypassZones = append(p.bypassZones, zone) +} + +// AddHost specifies a host name that will use the bypass proxy. +func (p *proxy_PerHost) AddHost(host string) { + if strings.HasSuffix(host, ".") { + host = host[:len(host)-1] + } + p.bypassHosts = append(p.bypassHosts, host) +} + +// A Dialer is a means to establish a connection. +type proxy_Dialer interface { + // Dial connects to the given address via the proxy. + Dial(network, addr string) (c net.Conn, err error) +} + +// Auth contains authentication parameters that specific Dialers may require. +type proxy_Auth struct { + User, Password string +} + +// FromEnvironment returns the dialer specified by the proxy related variables in +// the environment. +func proxy_FromEnvironment() proxy_Dialer { + allProxy := proxy_allProxyEnv.Get() + if len(allProxy) == 0 { + return proxy_Direct + } + + proxyURL, err := url.Parse(allProxy) + if err != nil { + return proxy_Direct + } + proxy, err := proxy_FromURL(proxyURL, proxy_Direct) + if err != nil { + return proxy_Direct + } + + noProxy := proxy_noProxyEnv.Get() + if len(noProxy) == 0 { + return proxy + } + + perHost := proxy_NewPerHost(proxy, proxy_Direct) + perHost.AddFromString(noProxy) + return perHost +} + +// proxySchemes is a map from URL schemes to a function that creates a Dialer +// from a URL with such a scheme. +var proxy_proxySchemes map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error) + +// RegisterDialerType takes a URL scheme and a function to generate Dialers from +// a URL with that scheme and a forwarding Dialer. Registered schemes are used +// by FromURL. +func proxy_RegisterDialerType(scheme string, f func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) { + if proxy_proxySchemes == nil { + proxy_proxySchemes = make(map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) + } + proxy_proxySchemes[scheme] = f +} + +// FromURL returns a Dialer given a URL specification and an underlying +// Dialer for it to make network requests. +func proxy_FromURL(u *url.URL, forward proxy_Dialer) (proxy_Dialer, error) { + var auth *proxy_Auth + if u.User != nil { + auth = new(proxy_Auth) + auth.User = u.User.Username() + if p, ok := u.User.Password(); ok { + auth.Password = p + } + } + + switch u.Scheme { + case "socks5": + return proxy_SOCKS5("tcp", u.Host, auth, forward) + } + + // If the scheme doesn't match any of the built-in schemes, see if it + // was registered by another package. + if proxy_proxySchemes != nil { + if f, ok := proxy_proxySchemes[u.Scheme]; ok { + return f(u, forward) + } + } + + return nil, errors.New("proxy: unknown scheme: " + u.Scheme) +} + +var ( + proxy_allProxyEnv = &proxy_envOnce{ + names: []string{"ALL_PROXY", "all_proxy"}, + } + proxy_noProxyEnv = &proxy_envOnce{ + names: []string{"NO_PROXY", "no_proxy"}, + } +) + +// envOnce looks up an environment variable (optionally by multiple +// names) once. It mitigates expensive lookups on some platforms +// (e.g. Windows). +// (Borrowed from net/http/transport.go) +type proxy_envOnce struct { + names []string + once sync.Once + val string +} + +func (e *proxy_envOnce) Get() string { + e.once.Do(e.init) + return e.val +} + +func (e *proxy_envOnce) init() { + for _, n := range e.names { + e.val = os.Getenv(n) + if e.val != "" { + return + } + } +} + +// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address +// with an optional username and password. See RFC 1928 and RFC 1929. +func proxy_SOCKS5(network, addr string, auth *proxy_Auth, forward proxy_Dialer) (proxy_Dialer, error) { + s := &proxy_socks5{ + network: network, + addr: addr, + forward: forward, + } + if auth != nil { + s.user = auth.User + s.password = auth.Password + } + + return s, nil +} + +type proxy_socks5 struct { + user, password string + network, addr string + forward proxy_Dialer +} + +const proxy_socks5Version = 5 + +const ( + proxy_socks5AuthNone = 0 + proxy_socks5AuthPassword = 2 +) + +const proxy_socks5Connect = 1 + +const ( + proxy_socks5IP4 = 1 + proxy_socks5Domain = 3 + proxy_socks5IP6 = 4 +) + +var proxy_socks5Errors = []string{ + "", + "general failure", + "connection forbidden", + "network unreachable", + "host unreachable", + "connection refused", + "TTL expired", + "command not supported", + "address type not supported", +} + +// Dial connects to the address addr on the given network via the SOCKS5 proxy. +func (s *proxy_socks5) Dial(network, addr string) (net.Conn, error) { + switch network { + case "tcp", "tcp6", "tcp4": + default: + return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network) + } + + conn, err := s.forward.Dial(s.network, s.addr) + if err != nil { + return nil, err + } + if err := s.connect(conn, addr); err != nil { + conn.Close() + return nil, err + } + return conn, nil +} + +// connect takes an existing connection to a socks5 proxy server, +// and commands the server to extend that connection to target, +// which must be a canonical address with a host and port. +func (s *proxy_socks5) connect(conn net.Conn, target string) error { + host, portStr, err := net.SplitHostPort(target) + if err != nil { + return err + } + + port, err := strconv.Atoi(portStr) + if err != nil { + return errors.New("proxy: failed to parse port number: " + portStr) + } + if port < 1 || port > 0xffff { + return errors.New("proxy: port number out of range: " + portStr) + } + + // the size here is just an estimate + buf := make([]byte, 0, 6+len(host)) + + buf = append(buf, proxy_socks5Version) + if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 { + buf = append(buf, 2 /* num auth methods */, proxy_socks5AuthNone, proxy_socks5AuthPassword) + } else { + buf = append(buf, 1 /* num auth methods */, proxy_socks5AuthNone) + } + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + if buf[0] != 5 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0]))) + } + if buf[1] == 0xff { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication") + } + + // See RFC 1929 + if buf[1] == proxy_socks5AuthPassword { + buf = buf[:0] + buf = append(buf, 1 /* password protocol version */) + buf = append(buf, uint8(len(s.user))) + buf = append(buf, s.user...) + buf = append(buf, uint8(len(s.password))) + buf = append(buf, s.password...) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if buf[1] != 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password") + } + } + + buf = buf[:0] + buf = append(buf, proxy_socks5Version, proxy_socks5Connect, 0 /* reserved */) + + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + buf = append(buf, proxy_socks5IP4) + ip = ip4 + } else { + buf = append(buf, proxy_socks5IP6) + } + buf = append(buf, ip...) + } else { + if len(host) > 255 { + return errors.New("proxy: destination host name too long: " + host) + } + buf = append(buf, proxy_socks5Domain) + buf = append(buf, byte(len(host))) + buf = append(buf, host...) + } + buf = append(buf, byte(port>>8), byte(port)) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:4]); err != nil { + return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + failure := "unknown error" + if int(buf[1]) < len(proxy_socks5Errors) { + failure = proxy_socks5Errors[buf[1]] + } + + if len(failure) > 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure) + } + + bytesToDiscard := 0 + switch buf[3] { + case proxy_socks5IP4: + bytesToDiscard = net.IPv4len + case proxy_socks5IP6: + bytesToDiscard = net.IPv6len + case proxy_socks5Domain: + _, err := io.ReadFull(conn, buf[:1]) + if err != nil { + return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + bytesToDiscard = int(buf[0]) + default: + return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr) + } + + if cap(buf) < bytesToDiscard { + buf = make([]byte, bytesToDiscard) + } else { + buf = buf[:bytesToDiscard] + } + if _, err := io.ReadFull(conn, buf); err != nil { + return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + // Also need to discard the port number + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + return nil +} diff --git a/vendor/github.com/nlopes/slack/.gitignore b/vendor/github.com/nlopes/slack/.gitignore new file mode 100644 index 0000000..ac6f3ee --- /dev/null +++ b/vendor/github.com/nlopes/slack/.gitignore @@ -0,0 +1,3 @@ +*.test +*~ +.idea/ 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/CHANGELOG.md b/vendor/github.com/nlopes/slack/CHANGELOG.md new file mode 100644 index 0000000..a79ea50 --- /dev/null +++ b/vendor/github.com/nlopes/slack/CHANGELOG.md @@ -0,0 +1,25 @@ +### v0.3.0 - July 30, 2018 +full differences can be viewed using `git log --oneline --decorate --color v0.2.0..v0.3.0` +- slack events initial support added. (still considered experimental and undergoing changes, stability not promised) +- vendored depedencies using dep, ensure using up to date tooling before filing issues. +- RTM has improved its ability to identify dead connections and reconnect automatically (worth calling out in case it has unintended side effects). +- bug fixes (various timestamp handling, error handling, RTM locking, etc). + +### v0.2.0 - Feb 10, 2018 + +Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against. + +Please check [0.2.0](https://github.com/nlopes/slack/releases/tag/v0.2.0) + +### v0.1.0 - May 28, 2017 + +This is released before adding context support. +As the used context package is the one from Go 1.7 this will be the last +compatible with Go < 1.7. + +Please check [0.1.0](https://github.com/nlopes/slack/releases/tag/v0.1.0) + +### v0.0.1 - Jul 26, 2015 + +If you just updated from master and it broke your implementation, please +check [0.0.1](https://github.com/nlopes/slack/releases/tag/v0.0.1) diff --git a/vendor/github.com/nlopes/slack/Gopkg.lock b/vendor/github.com/nlopes/slack/Gopkg.lock new file mode 100644 index 0000000..5cc0520 --- /dev/null +++ b/vendor/github.com/nlopes/slack/Gopkg.lock @@ -0,0 +1,33 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/davecgh/go-spew" + packages = ["spew"] + revision = "346938d642f2ec3594ed81d874461961cd0faa76" + version = "v1.1.0" + +[[projects]] + name = "github.com/gorilla/websocket" + packages = ["."] + revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" + version = "v1.2.0" + +[[projects]] + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + +[[projects]] + name = "github.com/stretchr/testify" + packages = ["assert"] + revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" + version = "v1.2.2" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "888307bf47ee004aaaa4c45e6139929b4984f2253e48e382246bfb8c66f3cd65" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/vendor/github.com/nlopes/slack/Gopkg.toml b/vendor/github.com/nlopes/slack/Gopkg.toml new file mode 100644 index 0000000..5271019 --- /dev/null +++ b/vendor/github.com/nlopes/slack/Gopkg.toml @@ -0,0 +1,13 @@ +ignored = ["github.com/lusis/slack-test"] + +[[constraint]] + name = "github.com/gorilla/websocket" + version = "1.2.0" + +[[constraint]] + name = "github.com/stretchr/testify" + version = "1.2.1" + +[prune] + go-tests = true + unused-packages = true diff --git a/vendor/github.com/nlopes/slack/LICENSE b/vendor/github.com/nlopes/slack/LICENSE new file mode 100644 index 0000000..5145171 --- /dev/null +++ b/vendor/github.com/nlopes/slack/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015, Norberto Lopes +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. 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/nlopes/slack/README.md b/vendor/github.com/nlopes/slack/README.md new file mode 100644 index 0000000..849e8bd --- /dev/null +++ b/vendor/github.com/nlopes/slack/README.md @@ -0,0 +1,95 @@ +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. + + + +## Change log +Support for the EventsAPI has recently been added. It is still in its early stages but nearly all events have been added and tested (except for those events in [Developer Preview](https://api.slack.com/slack-apps-preview) mode). API stability for events is not promised at this time. + +### v0.2.0 - Feb 10, 2018 + +Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against. + +Please check [0.2.0](https://github.com/nlopes/slack/releases/tag/v0.2.0) + +### CHANGELOG.md + + [CHANGELOG.md](https://github.com/nlopes/slack/blob/master/CHANGELOG.md) is available. Please visit it for updates. + +## Installing + +### *go get* + + $ go get -u github.com/nlopes/slack + +## Example + +### Getting all groups + +```golang +import ( + "fmt" + + "github.com/nlopes/slack" +) + +func main() { + api := slack.New("YOUR_TOKEN_HERE") + // If you set debugging, it will log all requests to the console + // Useful when encountering issues + // api.SetDebug(true) + groups, err := api.GetGroups(false) + if err != nil { + fmt.Printf("%s\n", err) + return + } + for _, group := range groups { + fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name) + } +} +``` + +### Getting User Information + +```golang +import ( + "fmt" + + "github.com/nlopes/slack" +) + +func main() { + api := slack.New("YOUR_TOKEN_HERE") + user, err := api.GetUserInfo("U023BECGF") + if err != nil { + fmt.Printf("%s\n", err) + return + } + fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email) +} +``` + +## Minimal RTM usage: + +See https://github.com/nlopes/slack/blob/master/examples/websocket/websocket.go + + +## Minimal EventsAPI usage: + +See https://github.com/nlopes/slack/blob/master/examples/eventsapi/events.go + + +## Contributing + +You are more than welcome to contribute to this project. Fork and +make a Pull Request, or create an Issue if you see any problem. + +## License + +BSD 2 Clause license diff --git a/vendor/github.com/nlopes/slack/TODO.txt b/vendor/github.com/nlopes/slack/TODO.txt new file mode 100644 index 0000000..8607960 --- /dev/null +++ b/vendor/github.com/nlopes/slack/TODO.txt @@ -0,0 +1,3 @@ +- Add more tests!!! +- Add support to have markdown hints + - See section Message Formatting at https://api.slack.com/docs/formatting diff --git a/vendor/github.com/nlopes/slack/admin.go b/vendor/github.com/nlopes/slack/admin.go new file mode 100644 index 0000000..a2aa7e5 --- /dev/null +++ b/vendor/github.com/nlopes/slack/admin.go @@ -0,0 +1,216 @@ +package slack + +import ( + "context" + "errors" + "fmt" + "net/url" +) + +type adminResponse struct { + OK bool `json:"ok"` + Error string `json:"error"` +} + +func adminRequest(ctx context.Context, client HTTPRequester, method string, teamName string, values url.Values, debug bool) (*adminResponse, error) { + adminResponse := &adminResponse{} + err := parseAdminResponse(ctx, client, method, teamName, values, adminResponse, debug) + if err != nil { + return nil, err + } + + if !adminResponse.OK { + return nil, errors.New(adminResponse.Error) + } + + return adminResponse, nil +} + +// DisableUser disabled a user account, given a user ID +func (api *Client) DisableUser(teamName string, uid string) error { + return api.DisableUserContext(context.Background(), teamName, uid) +} + +// DisableUserContext disabled a user account, given a user ID with a custom context +func (api *Client) DisableUserContext(ctx context.Context, teamName string, uid string) error { + values := url.Values{ + "user": {uid}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, err := adminRequest(ctx, api.httpclient, "setInactive", teamName, values, api.debug) + if err != nil { + return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err) + } + + return nil +} + +// InviteGuest invites a user to Slack as a single-channel guest +func (api *Client) InviteGuest(teamName, channel, firstName, lastName, emailAddress string) error { + return api.InviteGuestContext(context.Background(), teamName, channel, firstName, lastName, emailAddress) +} + +// InviteGuestContext invites a user to Slack as a single-channel guest with a custom context +func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, firstName, lastName, emailAddress string) error { + values := url.Values{ + "email": {emailAddress}, + "channels": {channel}, + "first_name": {firstName}, + "last_name": {lastName}, + "ultra_restricted": {"1"}, + "token": {api.token}, + "resend": {"true"}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) + if err != nil { + return fmt.Errorf("Failed to invite single-channel guest: %s", err) + } + + return nil +} + +// InviteRestricted invites a user to Slack as a restricted account +func (api *Client) InviteRestricted(teamName, channel, firstName, lastName, emailAddress string) error { + return api.InviteRestrictedContext(context.Background(), teamName, channel, firstName, lastName, emailAddress) +} + +// InviteRestrictedContext invites a user to Slack as a restricted account with a custom context +func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channel, firstName, lastName, emailAddress string) error { + values := url.Values{ + "email": {emailAddress}, + "channels": {channel}, + "first_name": {firstName}, + "last_name": {lastName}, + "restricted": {"1"}, + "token": {api.token}, + "resend": {"true"}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) + if err != nil { + return fmt.Errorf("Failed to restricted account: %s", err) + } + + return nil +} + +// InviteToTeam invites a user to a Slack team +func (api *Client) InviteToTeam(teamName, firstName, lastName, emailAddress string) error { + return api.InviteToTeamContext(context.Background(), teamName, firstName, lastName, emailAddress) +} + +// InviteToTeamContext invites a user to a Slack team with a custom context +func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName, lastName, emailAddress string) error { + values := url.Values{ + "email": {emailAddress}, + "first_name": {firstName}, + "last_name": {lastName}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) + if err != nil { + return fmt.Errorf("Failed to invite to team: %s", err) + } + + return nil +} + +// SetRegular enables the specified user +func (api *Client) SetRegular(teamName, user string) error { + return api.SetRegularContext(context.Background(), teamName, user) +} + +// SetRegularContext enables the specified user with a custom context +func (api *Client) SetRegularContext(ctx context.Context, teamName, user string) error { + values := url.Values{ + "user": {user}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, 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) + } + + return nil +} + +// SendSSOBindingEmail sends an SSO binding email to the specified user +func (api *Client) SendSSOBindingEmail(teamName, user string) error { + return api.SendSSOBindingEmailContext(context.Background(), teamName, user) +} + +// SendSSOBindingEmailContext sends an SSO binding email to the specified user with a custom context +func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, user string) error { + values := url.Values{ + "user": {user}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, 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) + } + + return nil +} + +// SetUltraRestricted converts a user into a single-channel guest +func (api *Client) SetUltraRestricted(teamName, uid, channel string) error { + return api.SetUltraRestrictedContext(context.Background(), teamName, uid, channel) +} + +// SetUltraRestrictedContext converts a user into a single-channel guest with a custom context +func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid, channel string) error { + values := url.Values{ + "user": {uid}, + "channel": {channel}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, err := adminRequest(ctx, api.httpclient, "setUltraRestricted", teamName, values, api.debug) + if err != nil { + return fmt.Errorf("Failed to ultra-restrict account: %s", err) + } + + return nil +} + +// SetRestricted converts a user into a restricted account +func (api *Client) SetRestricted(teamName, uid string) error { + return api.SetRestrictedContext(context.Background(), teamName, uid) +} + +// SetRestrictedContext converts a user into a restricted account with a custom context +func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid string) error { + values := url.Values{ + "user": {uid}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, err := adminRequest(ctx, api.httpclient, "setRestricted", teamName, values, api.debug) + if err != nil { + return fmt.Errorf("Failed to restrict account: %s", err) + } + + return nil +} diff --git a/vendor/github.com/nlopes/slack/attachments.go b/vendor/github.com/nlopes/slack/attachments.go new file mode 100644 index 0000000..326fc01 --- /dev/null +++ b/vendor/github.com/nlopes/slack/attachments.go @@ -0,0 +1,103 @@ +package slack + +import "encoding/json" + +// AttachmentField contains information for an attachment field +// An Attachment can contain multiple of these +type AttachmentField struct { + Title string `json:"title"` + Value string `json:"value"` + Short bool `json:"short"` +} + +// AttachmentAction is a button or menu to be included in the attachment. Required when +// using message buttons or menus and otherwise not useful. A maximum of 5 actions may be +// provided per attachment. +type AttachmentAction struct { + Name string `json:"name"` // Required. + Text string `json:"text"` // Required. + Style string `json:"style,omitempty"` // Optional. Allowed values: "default", "primary", "danger". + Type string `json:"type"` // Required. Must be set to "button" or "select". + Value string `json:"value,omitempty"` // Optional. + DataSource string `json:"data_source,omitempty"` // Optional. + MinQueryLength int `json:"min_query_length,omitempty"` // Optional. Default value is 1. + Options []AttachmentActionOption `json:"options,omitempty"` // Optional. Maximum of 100 options can be provided in each menu. + 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. +type AttachmentActionOption struct { + Text string `json:"text"` // Required. + Value string `json:"value"` // Required. + Description string `json:"description,omitempty"` // Optional. Up to 30 characters. +} + +// AttachmentActionOptionGroup is a semi-hierarchal way to list available options to appear in action menu. +type AttachmentActionOptionGroup struct { + Text string `json:"text"` // Required. + Options []AttachmentActionOption `json:"options"` // Required. +} + +// AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction) +type AttachmentActionCallback struct { + Actions []AttachmentAction `json:"actions"` + CallbackID string `json:"callback_id"` + Team Team `json:"team"` + Channel Channel `json:"channel"` + User User `json:"user"` + + Name string `json:"name"` + Value string `json:"value"` + + OriginalMessage Message `json:"original_message"` + + ActionTs string `json:"action_ts"` + MessageTs string `json:"message_ts"` + AttachmentID string `json:"attachment_id"` + Token string `json:"token"` + ResponseURL string `json:"response_url"` + TriggerID string `json:"trigger_id"` +} + +// ConfirmationField are used to ask users to confirm actions +type ConfirmationField struct { + Title string `json:"title,omitempty"` // Optional. + Text string `json:"text"` // Required. + OkText string `json:"ok_text,omitempty"` // Optional. Defaults to "Okay" + DismissText string `json:"dismiss_text,omitempty"` // Optional. Defaults to "Cancel" +} + +// Attachment contains all the information for an attachment +type Attachment struct { + Color string `json:"color,omitempty"` + Fallback string `json:"fallback"` + + CallbackID string `json:"callback_id,omitempty"` + ID int `json:"id,omitempty"` + + AuthorID string `json:"author_id,omitempty"` + AuthorName string `json:"author_name,omitempty"` + AuthorSubname string `json:"author_subname,omitempty"` + AuthorLink string `json:"author_link,omitempty"` + AuthorIcon string `json:"author_icon,omitempty"` + + Title string `json:"title,omitempty"` + TitleLink string `json:"title_link,omitempty"` + Pretext string `json:"pretext,omitempty"` + Text string `json:"text"` + + ImageURL string `json:"image_url,omitempty"` + ThumbURL string `json:"thumb_url,omitempty"` + + Fields []AttachmentField `json:"fields,omitempty"` + Actions []AttachmentAction `json:"actions,omitempty"` + MarkdownIn []string `json:"mrkdwn_in,omitempty"` + + Footer string `json:"footer,omitempty"` + FooterIcon string `json:"footer_icon,omitempty"` + + Ts json.Number `json:"ts,omitempty"` +} diff --git a/vendor/github.com/nlopes/slack/backoff.go b/vendor/github.com/nlopes/slack/backoff.go new file mode 100644 index 0000000..197bce2 --- /dev/null +++ b/vendor/github.com/nlopes/slack/backoff.go @@ -0,0 +1,57 @@ +package slack + +import ( + "math" + "math/rand" + "time" +) + +// This one was ripped from https://github.com/jpillora/backoff/blob/master/backoff.go + +// Backoff is a time.Duration counter. It starts at Min. After every +// call to Duration() it is multiplied by Factor. It is capped at +// Max. It returns to Min on every call to Reset(). Used in +// conjunction with the time package. +type backoff struct { + attempts int + //Factor is the multiplying factor for each increment step + Factor float64 + //Jitter eases contention by randomizing backoff steps + Jitter bool + //Min and Max are the minimum and maximum values of the counter + Min, Max time.Duration +} + +// Returns the current value of the counter and then multiplies it +// Factor +func (b *backoff) Duration() time.Duration { + //Zero-values are nonsensical, so we use + //them to apply defaults + if b.Min == 0 { + b.Min = 100 * time.Millisecond + } + if b.Max == 0 { + b.Max = 10 * time.Second + } + if b.Factor == 0 { + b.Factor = 2 + } + //calculate this duration + dur := float64(b.Min) * math.Pow(b.Factor, float64(b.attempts)) + if b.Jitter { + dur = rand.Float64()*(dur-float64(b.Min)) + float64(b.Min) + } + //cap! + if dur > float64(b.Max) { + return b.Max + } + //bump attempts count + b.attempts++ + //return as a time.Duration + return time.Duration(dur) +} + +//Resets the current value of the counter back to Min +func (b *backoff) Reset() { + b.attempts = 0 +} diff --git a/vendor/github.com/nlopes/slack/bots.go b/vendor/github.com/nlopes/slack/bots.go new file mode 100644 index 0000000..dfeaafb --- /dev/null +++ b/vendor/github.com/nlopes/slack/bots.go @@ -0,0 +1,51 @@ +package slack + +import ( + "context" + "errors" + "net/url" +) + +// Bot contains information about a bot +type Bot struct { + ID string `json:"id"` + Name string `json:"name"` + Deleted bool `json:"deleted"` + Icons Icons `json:"icons"` +} + +type botResponseFull struct { + Bot `json:"bot,omitempty"` // GetBotInfo + SlackResponse +} + +func botRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*botResponseFull, error) { + response := &botResponseFull{} + err := postPath(ctx, client, path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// GetBotInfo will retrieve the complete bot information +func (api *Client) GetBotInfo(bot string) (*Bot, error) { + return api.GetBotInfoContext(context.Background(), bot) +} + +// 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.token}, + "bot": {bot}, + } + + response, err := botRequest(ctx, api.httpclient, "bots.info", values, api.debug) + if err != nil { + return nil, err + } + return &response.Bot, nil +} diff --git a/vendor/github.com/nlopes/slack/channels.go b/vendor/github.com/nlopes/slack/channels.go new file mode 100644 index 0000000..6204315 --- /dev/null +++ b/vendor/github.com/nlopes/slack/channels.go @@ -0,0 +1,382 @@ +package slack + +import ( + "context" + "errors" + "net/url" + "strconv" +) + +type channelResponseFull struct { + Channel Channel `json:"channel"` + Channels []Channel `json:"channels"` + Purpose string `json:"purpose"` + Topic string `json:"topic"` + NotInChannel bool `json:"not_in_channel"` + History + SlackResponse +} + +// 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"` + Locale string `json:"locale"` +} + +func channelRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*channelResponseFull, error) { + response := &channelResponseFull{} + err := postForm(ctx, client, SLACK_API+path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// ArchiveChannel archives the given 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 +// see https://api.slack.com/methods/channels.archive +func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) (err error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + + _, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug) + return err +} + +// UnarchiveChannel unarchives the given 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 +// see https://api.slack.com/methods/channels.unarchive +func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string) (err error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + + _, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug) + return err +} + +// CreateChannel creates a channel with the given name and returns a *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 +// see https://api.slack.com/methods/channels.create +func (api *Client) CreateChannelContext(ctx context.Context, channelName string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "name": {channelName}, + } + + response, err := channelRequest(ctx, api.httpclient, "channels.create", values, api.debug) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// GetChannelHistory retrieves the channel history +// 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 +// 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.token}, + "channel": {channelID}, + } + if params.Latest != DEFAULT_HISTORY_LATEST { + values.Add("latest", params.Latest) + } + if params.Oldest != DEFAULT_HISTORY_OLDEST { + values.Add("oldest", params.Oldest) + } + if params.Count != DEFAULT_HISTORY_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { + if params.Inclusive { + values.Add("inclusive", "1") + } else { + values.Add("inclusive", "0") + } + } + + if params.Unreads != DEFAULT_HISTORY_UNREADS { + if params.Unreads { + values.Add("unreads", "1") + } else { + values.Add("unreads", "0") + } + } + + response, err := channelRequest(ctx, api.httpclient, "channels.history", values, api.debug) + if err != nil { + return nil, err + } + return &response.History, nil +} + +// GetChannelInfo retrieves the given 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 +// see https://api.slack.com/methods/channels.info +func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + + response, err := channelRequest(ctx, api.httpclient, "channels.info", values, api.debug) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// InviteUserToChannel invites a user to a given channel and returns a *Channel +// 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 +// 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.token}, + "channel": {channelID}, + "user": {user}, + } + + response, err := channelRequest(ctx, api.httpclient, "channels.invite", values, api.debug) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// JoinChannel joins the currently authenticated user to a 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 +// see https://api.slack.com/methods/channels.join +func (api *Client) JoinChannelContext(ctx context.Context, channelName string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "name": {channelName}, + } + + response, err := channelRequest(ctx, api.httpclient, "channels.join", values, api.debug) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// LeaveChannel makes the authenticated user leave the given 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 +// see https://api.slack.com/methods/channels.leave +func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (bool, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + + response, err := channelRequest(ctx, api.httpclient, "channels.leave", values, api.debug) + if err != nil { + return false, err + } + + return response.NotInChannel, nil +} + +// KickUserFromChannel kicks a user from a given channel +// 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 +// 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.token}, + "channel": {channelID}, + "user": {user}, + } + + _, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug) + return err +} + +// GetChannels retrieves all the channels +// see https://api.slack.com/methods/channels.list +func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) { + return api.GetChannelsContext(context.Background(), excludeArchived) +} + +// GetChannelsContext retrieves all the channels with a custom context +// see https://api.slack.com/methods/channels.list +func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool) ([]Channel, error) { + values := url.Values{ + "token": {api.token}, + } + if excludeArchived { + values.Add("exclude_archived", "1") + } + + response, err := channelRequest(ctx, api.httpclient, "channels.list", values, api.debug) + if err != nil { + return nil, err + } + return response.Channels, nil +} + +// SetChannelReadMark sets the read mark of a given channel to a specific point +// Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a +// 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. +// 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 +// 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.token}, + "channel": {channelID}, + "ts": {ts}, + } + + _, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug) + return err +} + +// RenameChannel renames a given channel +// 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 +// 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.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, api.httpclient, "channels.rename", values, api.debug) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// SetChannelPurpose sets the channel purpose and returns the purpose that was successfully set +// 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 +// 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.token}, + "channel": {channelID}, + "purpose": {purpose}, + } + + response, err := channelRequest(ctx, api.httpclient, "channels.setPurpose", values, api.debug) + if err != nil { + return "", err + } + return response.Purpose, nil +} + +// SetChannelTopic sets the channel topic and returns the topic that was successfully set +// 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 +// 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.token}, + "channel": {channelID}, + "topic": {topic}, + } + + response, err := channelRequest(ctx, api.httpclient, "channels.setTopic", values, api.debug) + if err != nil { + return "", err + } + return response.Topic, nil +} + +// GetChannelReplies gets an entire thread (a message plus all the messages in reply to it). +// 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 +// 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.token}, + "channel": {channelID}, + "thread_ts": {thread_ts}, + } + response, err := channelRequest(ctx, api.httpclient, "channels.replies", values, api.debug) + if err != nil { + return nil, err + } + return response.History.Messages, nil +} diff --git a/vendor/github.com/nlopes/slack/chat.go b/vendor/github.com/nlopes/slack/chat.go new file mode 100644 index 0000000..d62efef --- /dev/null +++ b/vendor/github.com/nlopes/slack/chat.go @@ -0,0 +1,448 @@ +package slack + +import ( + "context" + "encoding/json" + "net/url" + "strings" +) + +const ( + DEFAULT_MESSAGE_USERNAME = "" + 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 + DEFAULT_MESSAGE_ICON_URL = "" + DEFAULT_MESSAGE_ICON_EMOJI = "" + DEFAULT_MESSAGE_MARKDOWN = true + DEFAULT_MESSAGE_ESCAPE_TEXT = true +) + +type chatResponseFull struct { + Channel string `json:"channel"` + Timestamp string `json:"ts"` //Regualr message timestamp + MessageTimeStamp string `json:"message_ts"` //Ephemeral message timestamp + Text string `json:"text"` + SlackResponse +} + +// getMessageTimestamp will inspect the `chatResponseFull` to ruturn a timestamp value +// in `chat.postMessage` its under `ts` +// in `chat.postEphemeral` its under `message_ts` +func (c chatResponseFull) getMessageTimestamp() string { + if len(c.Timestamp) > 0 { + return c.Timestamp + } + return c.MessageTimeStamp +} + +// PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request +type PostMessageParameters struct { + Username string `json:"username"` + AsUser bool `json:"as_user"` + Parse string `json:"parse"` + ThreadTimestamp string `json:"thread_ts"` + ReplyBroadcast bool `json:"reply_broadcast"` + LinkNames int `json:"link_names"` + Attachments []Attachment `json:"attachments"` + UnfurlLinks bool `json:"unfurl_links"` + UnfurlMedia bool `json:"unfurl_media"` + IconURL string `json:"icon_url"` + IconEmoji string `json:"icon_emoji"` + Markdown bool `json:"mrkdwn,omitempty"` + EscapeText bool `json:"escape_text"` + + // 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, + 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, + } +} + +// DeleteMessage deletes a message in a channel +func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext(context.Background(), channel, MsgOptionDelete(messageTimestamp)) + return respChannel, respTimestamp, err +} + +// DeleteMessageContext deletes a message in a channel with a custom context +func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTimestamp string) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext(ctx, channel, MsgOptionDelete(messageTimestamp)) + return respChannel, respTimestamp, err +} + +// PostMessage sends a message to a channel. +// Message is escaped by default according to https://api.slack.com/docs/formatting +// Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. +func (api *Client) PostMessage(channel, text string, params PostMessageParameters) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext( + context.Background(), + channel, + MsgOptionText(text, params.EscapeText), + MsgOptionAttachments(params.Attachments...), + MsgOptionPostMessageParameters(params), + ) + return respChannel, respTimestamp, err +} + +// PostMessageContext sends a message to a channel with a custom context +// For more details, see PostMessage documentation +func (api *Client) PostMessageContext(ctx context.Context, channel, text string, params PostMessageParameters) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext( + ctx, + channel, + MsgOptionText(text, params.EscapeText), + MsgOptionAttachments(params.Attachments...), + MsgOptionPostMessageParameters(params), + ) + 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(channelID, userID string, options ...MsgOption) (string, error) { + return api.PostEphemeralContext( + context.Background(), + channelID, + 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, channelID, userID string, options ...MsgOption) (timestamp string, err error) { + _, timestamp, _, err = api.SendMessageContext(ctx, channelID, append(options, MsgOptionPostEphemeral2(userID))...) + return timestamp, err +} + +// UpdateMessage updates a message in a channel +func (api *Client) UpdateMessage(channelID, timestamp, text string) (string, string, string, error) { + return api.UpdateMessageContext(context.Background(), channelID, timestamp, text) +} + +// UpdateMessageContext updates a message in a channel +func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp, text string) (string, string, string, error) { + return api.SendMessageContext(ctx, channelID, MsgOptionUpdate(timestamp), MsgOptionText(text, true)) +} + +// SendMessage more flexible method for configuring messages. +func (api *Client) SendMessage(channel string, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext(context.Background(), channel, options...) +} + +// SendMessageContext more flexible method for configuring messages with a custom context. +func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (channel string, timestamp string, text string, err error) { + var ( + config sendConfig + response chatResponseFull + ) + + if config, err = applyMsgOptions(api.token, channelID, options...); err != nil { + return "", "", "", err + } + + if err = postPath(ctx, api.httpclient, string(config.mode), config.values, &response, api.debug); err != nil { + return "", "", "", err + } + + return response.Channel, response.getMessageTimestamp(), response.Text, response.Err() +} + +// ApplyMsgOptions utility function for debugging/testing chat requests. +func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) { + config, err := applyMsgOptions(token, channel, options...) + return string(config.mode), config.values, err +} + +func applyMsgOptions(token, channel string, options ...MsgOption) (sendConfig, error) { + config := sendConfig{ + mode: chatPostMessage, + values: url.Values{ + "token": {token}, + "channel": {channel}, + }, + } + + for _, opt := range options { + if err := opt(&config); err != nil { + return config, err + } + } + + return config, nil +} + +func escapeMessage(message string) string { + replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">") + return replacer.Replace(message) +} + +type sendMode string + +const ( + chatUpdate sendMode = "chat.update" + chatPostMessage sendMode = "chat.postMessage" + chatDelete sendMode = "chat.delete" + chatPostEphemeral sendMode = "chat.postEphemeral" + chatMeMessage sendMode = "chat.meMessage" +) + +type sendConfig struct { + mode sendMode + values url.Values +} + +// MsgOption option provided when sending a message. +type MsgOption func(*sendConfig) error + +// MsgOptionPost posts a messages, this is the default. +func MsgOptionPost() MsgOption { + return func(config *sendConfig) error { + config.mode = chatPostMessage + config.values.Del("ts") + return nil + } +} + +// MsgOptionPostEphemeral - DEPRECATED: use MsgOptionPostEphemeral2 +// posts an ephemeral message. +func MsgOptionPostEphemeral() MsgOption { + return func(config *sendConfig) error { + config.mode = chatPostEphemeral + config.values.Del("ts") + return nil + } +} + +// MsgOptionPostEphemeral2 - posts an ephemeral message to the provided user. +func MsgOptionPostEphemeral2(userID string) MsgOption { + return func(config *sendConfig) error { + config.mode = chatPostEphemeral + MsgOptionUser(userID)(config) + config.values.Del("ts") + + return nil + } +} + +// MsgOptionMeMessage posts a "me message" type from the calling user +func MsgOptionMeMessage() MsgOption { + return func(config *sendConfig) error { + config.mode = chatMeMessage + return nil + } +} + +// MsgOptionUpdate updates a message based on the timestamp. +func MsgOptionUpdate(timestamp string) MsgOption { + return func(config *sendConfig) error { + config.mode = chatUpdate + config.values.Add("ts", timestamp) + return nil + } +} + +// MsgOptionDelete deletes a message based on the timestamp. +func MsgOptionDelete(timestamp string) MsgOption { + return func(config *sendConfig) error { + config.mode = chatDelete + config.values.Add("ts", timestamp) + return nil + } +} + +// MsgOptionAsUser whether or not to send the message as the user. +func MsgOptionAsUser(b bool) MsgOption { + return func(config *sendConfig) error { + if b != DEFAULT_MESSAGE_ASUSER { + config.values.Set("as_user", "true") + } + return nil + } +} + +// MsgOptionUser set the user for the message. +func MsgOptionUser(userID string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("user", userID) + return nil + } +} + +// MsgOptionText provide the text for the message, optionally escape the provided +// text. +func MsgOptionText(text string, escape bool) MsgOption { + return func(config *sendConfig) error { + if escape { + text = escapeMessage(text) + } + config.values.Add("text", text) + return nil + } +} + +// MsgOptionAttachments provide attachments for the message. +func MsgOptionAttachments(attachments ...Attachment) MsgOption { + return func(config *sendConfig) error { + if attachments == nil { + return nil + } + + attachments, err := json.Marshal(attachments) + if err == nil { + config.values.Set("attachments", string(attachments)) + } + return err + } +} + +// MsgOptionEnableLinkUnfurl enables link unfurling +func MsgOptionEnableLinkUnfurl() MsgOption { + return func(config *sendConfig) error { + config.values.Set("unfurl_links", "true") + return nil + } +} + +// 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 { + config.values.Set("unfurl_media", "false") + return nil + } +} + +// MsgOptionDisableMarkdown disables markdown. +func MsgOptionDisableMarkdown() MsgOption { + return func(config *sendConfig) error { + config.values.Set("mrkdwn", "false") + return nil + } +} + +// MsgOptionTS sets the thread TS of the message to enable creating or replying to a thread +func MsgOptionTS(ts string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("thread_ts", ts) + return nil + } +} + +// MsgOptionBroadcast sets reply_broadcast to true +func MsgOptionBroadcast() MsgOption { + return func(config *sendConfig) error { + config.values.Set("reply_broadcast", "true") + return nil + } +} + +// this function combines multiple options into a single option. +func MsgOptionCompose(options ...MsgOption) MsgOption { + return func(c *sendConfig) error { + for _, opt := range options { + if err := opt(c); err != nil { + return err + } + } + return nil + } +} + +func MsgOptionParse(b bool) MsgOption { + return func(c *sendConfig) error { + var v string + if b { + v = "1" + } else { + v = "0" + } + c.values.Set("parse", v) + return nil + } +} + +// MsgOptionPostMessageParameters maintain backwards compatibility. +func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { + return func(config *sendConfig) error { + if params.Username != DEFAULT_MESSAGE_USERNAME { + config.values.Set("username", 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) + + if params.Parse != DEFAULT_MESSAGE_PARSE { + config.values.Set("parse", params.Parse) + } + if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES { + config.values.Set("link_names", "1") + } + + if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS { + config.values.Set("unfurl_links", "true") + } + + // I want to send a message with explicit `as_user` `true` and `unfurl_links` `false` in request. + // Because setting `as_user` to `true` will change the default value for `unfurl_links` to `true` on Slack API side. + if params.AsUser != DEFAULT_MESSAGE_ASUSER && params.UnfurlLinks == DEFAULT_MESSAGE_UNFURL_LINKS { + config.values.Set("unfurl_links", "false") + } + if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA { + config.values.Set("unfurl_media", "false") + } + if params.IconURL != DEFAULT_MESSAGE_ICON_URL { + config.values.Set("icon_url", params.IconURL) + } + if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI { + config.values.Set("icon_emoji", params.IconEmoji) + } + if params.Markdown != DEFAULT_MESSAGE_MARKDOWN { + config.values.Set("mrkdwn", "false") + } + + 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/comment.go b/vendor/github.com/nlopes/slack/comment.go new file mode 100644 index 0000000..7d1c0d4 --- /dev/null +++ b/vendor/github.com/nlopes/slack/comment.go @@ -0,0 +1,10 @@ +package slack + +// Comment contains all the information relative to a comment +type Comment struct { + ID string `json:"id,omitempty"` + Created JSONTime `json:"created,omitempty"` + Timestamp JSONTime `json:"timestamp,omitempty"` + User string `json:"user,omitempty"` + Comment string `json:"comment,omitempty"` +} diff --git a/vendor/github.com/nlopes/slack/conversation.go b/vendor/github.com/nlopes/slack/conversation.go new file mode 100644 index 0000000..d68e52f --- /dev/null +++ b/vendor/github.com/nlopes/slack/conversation.go @@ -0,0 +1,566 @@ +package slack + +import ( + "context" + "errors" + "net/url" + "strconv" + "strings" +) + +// Conversation is the foundation for IM and BaseGroupConversation +type conversation struct { + ID string `json:"id"` + Created JSONTime `json:"created"` + IsOpen bool `json:"is_open"` + LastRead string `json:"last_read,omitempty"` + 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 +type groupConversation struct { + conversation + Name string `json:"name"` + Creator string `json:"creator"` + IsArchived bool `json:"is_archived"` + Members []string `json:"members"` + Topic Topic `json:"topic"` + Purpose Purpose `json:"purpose"` +} + +// Topic contains information about the topic +type Topic struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet JSONTime `json:"last_set"` +} + +// Purpose contains information about the purpose +type Purpose struct { + Value string `json:"value"` + 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", strconv.Itoa(params.Limit)) + } + response := struct { + Members []string `json:"members"` + ResponseMetaData responseMetaData `json:"response_metadata"` + SlackResponse + }{} + err := postPath(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 := postPath(ctx, api.httpclient, "conversations.archive", values, &response, api.debug) + if err != nil { + return err + } + + return response.Err() +} + +// 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 := postPath(ctx, api.httpclient, "conversations.unarchive", values, &response, api.debug) + if err != nil { + return err + } + + return response.Err() +} + +// 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 := postPath(ctx, api.httpclient, "conversations.setTopic", values, &response, api.debug) + if err != nil { + return nil, err + } + + return response.Channel, response.Err() +} + +// 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 := postPath(ctx, api.httpclient, "conversations.setPurpose", values, &response, api.debug) + if err != nil { + return nil, err + } + + return response.Channel, response.Err() +} + +// 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 := postPath(ctx, api.httpclient, "conversations.rename", values, &response, api.debug) + if err != nil { + return nil, err + } + + return response.Channel, response.Err() +} + +// 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 := postPath(ctx, api.httpclient, "conversations.invite", values, &response, api.debug) + if err != nil { + return nil, err + } + + return response.Channel, response.Err() +} + +// 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 := postPath(ctx, api.httpclient, "conversations.kick", values, &response, api.debug) + if err != nil { + return err + } + + return response.Err() +} + +// 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 = postPath(ctx, api.httpclient, "conversations.close", values, &response, api.debug) + if err != nil { + return false, false, err + } + + return response.NoOp, response.AlreadyClosed, response.Err() +} + +// 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 + } + + return &response.Channel, response.Err() +} + +// 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 + } + + return &response.Channel, response.Err() +} + +// 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, err +} + +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", strconv.Itoa(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 = postPath(ctx, api.httpclient, "conversations.replies", values, &response, api.debug) + if err != nil { + return nil, false, "", err + } + + return response.Messages, response.HasMore, response.ResponseMetaData.NextCursor, response.Err() +} + +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", strconv.Itoa(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 = postPath(ctx, api.httpclient, "conversations.list", values, &response, api.debug) + if err != nil { + return nil, "", err + } + + return response.Channels, response.ResponseMetaData.NextCursor, response.Err() +} + +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 := postPath(ctx, api.httpclient, "conversations.open", values, &response, api.debug) + if err != nil { + return nil, false, false, err + } + + return response.Channel, response.NoOp, response.AlreadyOpen, response.Err() +} + +// 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 := postPath(ctx, api.httpclient, "conversations.join", values, &response, api.debug) + if err != nil { + return nil, "", nil, err + } + if response.Err() != nil { + return nil, "", nil, response.Err() + } + 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", strconv.Itoa(params.Limit)) + } + if params.Oldest != "" { + values.Add("oldest", params.Oldest) + } + + response := GetConversationHistoryResponse{} + + err := postPath(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/dialog.go b/vendor/github.com/nlopes/slack/dialog.go new file mode 100644 index 0000000..a13e53d --- /dev/null +++ b/vendor/github.com/nlopes/slack/dialog.go @@ -0,0 +1,107 @@ +package slack + +import ( + "context" + "encoding/json" + "errors" +) + +type DialogTrigger struct { + TriggerId string `json:"trigger_id"` //Required. Must respond within 3 seconds. + Dialog Dialog `json:"dialog"` //Required. +} + +type Dialog struct { + CallbackId string `json:"callback_id"` //Required. + Title string `json:"title"` //Required. + SubmitLabel string `json:"submit_label,omitempty"` //Optional. Default value is 'Submit' + NotifyOnCancel bool `json:"notify_on_cancel,omitempty"` //Optional. Default value is false + Elements []DialogElement `json:"elements"` //Required. +} + +type DialogElement interface{} + +type DialogTextElement struct { + Label string `json:"label"` //Required. + Name string `json:"name"` //Required. + Type string `json:"type"` //Required. Allowed values: "text", "textarea", "select". + Placeholder string `json:"placeholder,omitempty"` //Optional. + Optional bool `json:"optional,omitempty"` //Optional. Default value is false + Value string `json:"value,omitempty"` //Optional. + MaxLength int `json:"max_length,omitempty"` //Optional. + MinLength int `json:"min_length,omitempty"` //Optional,. Default value is 0 + Hint string `json:"hint,omitempty"` //Optional. + Subtype string `json:"subtype,omitempty"` //Optional. Allowed values: "email", "number", "tel", "url". +} + +type DialogSelectElement struct { + Label string `json:"label"` //Required. + Name string `json:"name"` //Required. + Type string `json:"type"` //Required. Allowed values: "text", "textarea", "select". + Placeholder string `json:"placeholder,omitempty"` //Optional. + Optional bool `json:"optional,omitempty"` //Optional. Default value is false + Value string `json:"value,omitempty"` //Optional. + DataSource string `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external". + SelectedOptions string `json:"selected_options,omitempty"` //Optional. Default value for "external" only + Options []DialogElementOption `json:"options,omitempty"` //One of options or option_groups is required. + OptionGroups []DialogElementOption `json:"option_groups,omitempty"` //Provide up to 100 options. +} + +type DialogElementOption struct { + Label string `json:"label"` //Required. + Value string `json:"value"` //Required. +} + +// DialogCallback is sent from Slack when a user submits a form from within a dialog +type DialogCallback struct { + Type string `json:"type"` + CallbackID string `json:"callback_id"` + Team Team `json:"team"` + Channel Channel `json:"channel"` + User User `json:"user"` + ActionTs string `json:"action_ts"` + Token string `json:"token"` + ResponseURL string `json:"response_url"` + Submission map[string]string `json:"submission"` +} + +// DialogSuggestionCallback is sent from Slack when a user types in a select field with an external data source +type DialogSuggestionCallback struct { + Type string `json:"type"` + Token string `json:"token"` + ActionTs string `json:"action_ts"` + Team Team `json:"team"` + User User `json:"user"` + Channel Channel `json:"channel"` + ElementName string `json:"name"` + Value string `json:"value"` + CallbackID string `json:"callback_id"` +} + +// OpenDialog opens a dialog window where the triggerId originated from +func (api *Client) OpenDialog(triggerId string, dialog Dialog) (err error) { + return api.OpenDialogContext(context.Background(), triggerId, dialog) +} + +// OpenDialogContext opens a dialog window where the triggerId originated from with a custom context +func (api *Client) OpenDialogContext(ctx context.Context, triggerId string, dialog Dialog) (err error) { + if triggerId == "" { + return errors.New("received empty parameters") + } + + resp := DialogTrigger{ + TriggerId: triggerId, + Dialog: dialog, + } + jsonResp, err := json.Marshal(resp) + if err != nil { + return err + } + response := &SlackResponse{} + endpoint := SLACK_API + "dialog.open" + if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonResp, response, api.debug); err != nil { + return err + } + + return response.Err() +} diff --git a/vendor/github.com/nlopes/slack/dnd.go b/vendor/github.com/nlopes/slack/dnd.go new file mode 100644 index 0000000..b7f14a8 --- /dev/null +++ b/vendor/github.com/nlopes/slack/dnd.go @@ -0,0 +1,152 @@ +package slack + +import ( + "context" + "errors" + "net/url" + "strconv" + "strings" +) + +type SnoozeDebug struct { + SnoozeEndDate string `json:"snooze_end_date"` +} + +type SnoozeInfo struct { + SnoozeEnabled bool `json:"snooze_enabled,omitempty"` + SnoozeEndTime int `json:"snooze_endtime,omitempty"` + SnoozeRemaining int `json:"snooze_remaining,omitempty"` + SnoozeDebug SnoozeDebug `json:"snooze_debug,omitempty"` +} + +type DNDStatus struct { + Enabled bool `json:"dnd_enabled"` + NextStartTimestamp int `json:"next_dnd_start_ts"` + NextEndTimestamp int `json:"next_dnd_end_ts"` + SnoozeInfo +} + +type dndResponseFull struct { + DNDStatus + SlackResponse +} + +type dndTeamInfoResponse struct { + Users map[string]DNDStatus `json:"users"` + SlackResponse +} + +func dndRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*dndResponseFull, error) { + response := &dndResponseFull{} + err := postPath(ctx, client, path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// EndDND ends the user's scheduled Do Not Disturb session +func (api *Client) EndDND() error { + return api.EndDNDContext(context.Background()) +} + +// 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.token}, + } + + response := &SlackResponse{} + + if err := postPath(ctx, api.httpclient, "dnd.endDnd", values, response, api.debug); err != nil { + return err + } + + return response.Err() +} + +// EndSnooze ends the current user's snooze mode +func (api *Client) EndSnooze() (*DNDStatus, error) { + return api.EndSnoozeContext(context.Background()) +} + +// 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.token}, + } + + response, err := dndRequest(ctx, api.httpclient, "dnd.endSnooze", values, api.debug) + if err != nil { + return nil, err + } + return &response.DNDStatus, nil +} + +// GetDNDInfo provides information about a user's current Do Not Disturb settings. +func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) { + return api.GetDNDInfoContext(context.Background(), user) +} + +// 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.token}, + } + if user != nil { + values.Set("user", *user) + } + + response, err := dndRequest(ctx, api.httpclient, "dnd.info", values, api.debug) + if err != nil { + return nil, err + } + return &response.DNDStatus, nil +} + +// GetDNDTeamInfo provides information about a user's current Do Not Disturb settings. +func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error) { + return api.GetDNDTeamInfoContext(context.Background(), users) +} + +// 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.token}, + "users": {strings.Join(users, ",")}, + } + response := &dndTeamInfoResponse{} + + if err := postPath(ctx, api.httpclient, "dnd.teamInfo", values, response, api.debug); err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response.Users, nil +} + +// SetSnooze adjusts the snooze duration for a user's Do Not Disturb +// settings. If a snooze session is not already active for the user, invoking +// this method will begin one for the specified duration. +func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) { + return api.SetSnoozeContext(context.Background(), minutes) +} + +// SetSnooze adjusts the snooze duration for a user's Do Not Disturb settings with a custom context. +// For more information see the SetSnooze docs +func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) { + values := url.Values{ + "token": {api.token}, + "num_minutes": {strconv.Itoa(minutes)}, + } + + response, err := dndRequest(ctx, api.httpclient, "dnd.setSnooze", values, api.debug) + if err != nil { + return nil, err + } + return &response.DNDStatus, nil +} diff --git a/vendor/github.com/nlopes/slack/emoji.go b/vendor/github.com/nlopes/slack/emoji.go new file mode 100644 index 0000000..0fff612 --- /dev/null +++ b/vendor/github.com/nlopes/slack/emoji.go @@ -0,0 +1,34 @@ +package slack + +import ( + "context" + "errors" + "net/url" +) + +type emojiResponseFull struct { + Emoji map[string]string `json:"emoji"` + SlackResponse +} + +// GetEmoji retrieves all the emojis +func (api *Client) GetEmoji() (map[string]string, error) { + return api.GetEmojiContext(context.Background()) +} + +// 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.token}, + } + response := &emojiResponseFull{} + + err := postPath(ctx, api.httpclient, "emoji.list", values, response, api.debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response.Emoji, nil +} diff --git a/vendor/github.com/nlopes/slack/files.go b/vendor/github.com/nlopes/slack/files.go new file mode 100644 index 0000000..2381ec3 --- /dev/null +++ b/vendor/github.com/nlopes/slack/files.go @@ -0,0 +1,332 @@ +package slack + +import ( + "context" + "errors" + "io" + "net/url" + "strconv" + "strings" +) + +const ( + // Add here the defaults in the siten + DEFAULT_FILES_USER = "" + DEFAULT_FILES_CHANNEL = "" + DEFAULT_FILES_TS_FROM = 0 + DEFAULT_FILES_TS_TO = -1 + DEFAULT_FILES_TYPES = "all" + DEFAULT_FILES_COUNT = 100 + DEFAULT_FILES_PAGE = 1 +) + +// File contains all the information for a file +type File struct { + ID string `json:"id"` + Created JSONTime `json:"created"` + Timestamp JSONTime `json:"timestamp"` + + Name string `json:"name"` + Title string `json:"title"` + Mimetype string `json:"mimetype"` + ImageExifRotation int `json:"image_exif_rotation"` + Filetype string `json:"filetype"` + PrettyType string `json:"pretty_type"` + User string `json:"user"` + + Mode string `json:"mode"` + Editable bool `json:"editable"` + IsExternal bool `json:"is_external"` + ExternalType string `json:"external_type"` + + Size int `json:"size"` + + URL string `json:"url"` // Deprecated - never set + URLDownload string `json:"url_download"` // Deprecated - never set + URLPrivate string `json:"url_private"` + URLPrivateDownload string `json:"url_private_download"` + + OriginalH int `json:"original_h"` + OriginalW int `json:"original_w"` + Thumb64 string `json:"thumb_64"` + Thumb80 string `json:"thumb_80"` + Thumb160 string `json:"thumb_160"` + Thumb360 string `json:"thumb_360"` + Thumb360Gif string `json:"thumb_360_gif"` + Thumb360W int `json:"thumb_360_w"` + Thumb360H int `json:"thumb_360_h"` + Thumb480 string `json:"thumb_480"` + Thumb480W int `json:"thumb_480_w"` + Thumb480H int `json:"thumb_480_h"` + Thumb720 string `json:"thumb_720"` + Thumb720W int `json:"thumb_720_w"` + Thumb720H int `json:"thumb_720_h"` + Thumb960 string `json:"thumb_960"` + Thumb960W int `json:"thumb_960_w"` + Thumb960H int `json:"thumb_960_h"` + Thumb1024 string `json:"thumb_1024"` + Thumb1024W int `json:"thumb_1024_w"` + Thumb1024H int `json:"thumb_1024_h"` + + Permalink string `json:"permalink"` + PermalinkPublic string `json:"permalink_public"` + + EditLink string `json:"edit_link"` + Preview string `json:"preview"` + PreviewHighlight string `json:"preview_highlight"` + Lines int `json:"lines"` + LinesMore int `json:"lines_more"` + + IsPublic bool `json:"is_public"` + PublicURLShared bool `json:"public_url_shared"` + Channels []string `json:"channels"` + Groups []string `json:"groups"` + IMs []string `json:"ims"` + InitialComment Comment `json:"initial_comment"` + CommentsCount int `json:"comments_count"` + NumStars int `json:"num_stars"` + IsStarred bool `json:"is_starred"` +} + +// FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request. +// +// There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large, +// or provide a local file path in File to upload it from your filesystem. +type FileUploadParameters struct { + File string + Content string + Reader io.Reader + Filetype string + Filename string + Title string + InitialComment string + Channels []string +} + +// GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request +type GetFilesParameters struct { + User string + Channel string + TimestampFrom JSONTime + TimestampTo JSONTime + Types string + Count int + Page int +} + +type fileResponseFull struct { + File `json:"file"` + Paging `json:"paging"` + Comments []Comment `json:"comments"` + Files []File `json:"files"` + + SlackResponse +} + +// NewGetFilesParameters provides an instance of GetFilesParameters with all the sane default values set +func NewGetFilesParameters() GetFilesParameters { + return GetFilesParameters{ + User: DEFAULT_FILES_USER, + Channel: DEFAULT_FILES_CHANNEL, + TimestampFrom: DEFAULT_FILES_TS_FROM, + TimestampTo: DEFAULT_FILES_TS_TO, + Types: DEFAULT_FILES_TYPES, + Count: DEFAULT_FILES_COUNT, + Page: DEFAULT_FILES_PAGE, + } +} + +func fileRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*fileResponseFull, error) { + response := &fileResponseFull{} + err := postForm(ctx, client, SLACK_API+path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// GetFileInfo retrieves a file and related comments +func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment, *Paging, error) { + return api.GetFileInfoContext(context.Background(), fileID, count, page) +} + +// 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.token}, + "file": {fileID}, + "count": {strconv.Itoa(count)}, + "page": {strconv.Itoa(page)}, + } + + response, err := fileRequest(ctx, api.httpclient, "files.info", values, api.debug) + if err != nil { + return nil, nil, nil, err + } + return &response.File, response.Comments, &response.Paging, nil +} + +// GetFiles retrieves all files according to the parameters given +func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) { + return api.GetFilesContext(context.Background(), params) +} + +// 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.token}, + } + if params.User != DEFAULT_FILES_USER { + values.Add("user", params.User) + } + if params.Channel != DEFAULT_FILES_CHANNEL { + values.Add("channel", params.Channel) + } + if params.TimestampFrom != DEFAULT_FILES_TS_FROM { + values.Add("ts_from", strconv.FormatInt(int64(params.TimestampFrom), 10)) + } + if params.TimestampTo != DEFAULT_FILES_TS_TO { + values.Add("ts_to", strconv.FormatInt(int64(params.TimestampTo), 10)) + } + if params.Types != DEFAULT_FILES_TYPES { + values.Add("types", params.Types) + } + if params.Count != DEFAULT_FILES_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Page != DEFAULT_FILES_PAGE { + values.Add("page", strconv.Itoa(params.Page)) + } + + response, err := fileRequest(ctx, api.httpclient, "files.list", values, api.debug) + if err != nil { + return nil, nil, err + } + return response.Files, &response.Paging, nil +} + +// UploadFile uploads a file +func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) { + return api.UploadFileContext(context.Background(), params) +} + +// UploadFileContext uploads a file and setting a custom context +func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParameters) (file *File, err error) { + // Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More + // investigation needed, but for now this will do. + _, err = api.AuthTest() + if err != nil { + return nil, err + } + response := &fileResponseFull{} + values := url.Values{ + "token": {api.token}, + } + if params.Filetype != "" { + values.Add("filetype", params.Filetype) + } + if params.Filename != "" { + values.Add("filename", params.Filename) + } + if params.Title != "" { + values.Add("title", params.Title) + } + if params.InitialComment != "" { + values.Add("initial_comment", params.InitialComment) + } + if len(params.Channels) != 0 { + values.Add("channels", strings.Join(params.Channels, ",")) + } + if params.Content != "" { + values.Add("content", params.Content) + err = postForm(ctx, api.httpclient, SLACK_API+"files.upload", values, response, api.debug) + } else if params.File != "" { + err = postLocalWithMultipartResponse(ctx, api.httpclient, "files.upload", params.File, "file", values, response, api.debug) + } else if params.Reader != nil { + err = postWithMultipartResponse(ctx, api.httpclient, "files.upload", params.Filename, "file", values, params.Reader, response, api.debug) + } + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + 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}, + } + _, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api.debug) + return err +} + +// 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) (err error) { + values := url.Values{ + "token": {api.token}, + "file": {fileID}, + } + + _, err = fileRequest(ctx, api.httpclient, "files.delete", values, api.debug) + return err +} + +// RevokeFilePublicURL disables public/external sharing for a file +func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) { + return api.RevokeFilePublicURLContext(context.Background(), fileID) +} + +// 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.token}, + "file": {fileID}, + } + + response, err := fileRequest(ctx, api.httpclient, "files.revokePublicURL", values, api.debug) + if err != nil { + return nil, err + } + return &response.File, nil +} + +// ShareFilePublicURL enabled public/external sharing for a file +func (api *Client) ShareFilePublicURL(fileID string) (*File, []Comment, *Paging, error) { + return api.ShareFilePublicURLContext(context.Background(), fileID) +} + +// 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.token}, + "file": {fileID}, + } + + response, err := fileRequest(ctx, api.httpclient, "files.sharedPublicURL", values, api.debug) + if err != nil { + return nil, nil, nil, err + } + return &response.File, response.Comments, &response.Paging, nil +} diff --git a/vendor/github.com/nlopes/slack/groups.go b/vendor/github.com/nlopes/slack/groups.go new file mode 100644 index 0000000..67e78e9 --- /dev/null +++ b/vendor/github.com/nlopes/slack/groups.go @@ -0,0 +1,376 @@ +package slack + +import ( + "context" + "errors" + "net/url" + "strconv" +) + +// Group contains all the information for a group +type Group struct { + groupConversation + IsGroup bool `json:"is_group"` +} + +type groupResponseFull struct { + Group Group `json:"group"` + Groups []Group `json:"groups"` + Purpose string `json:"purpose"` + Topic string `json:"topic"` + NotInGroup bool `json:"not_in_group"` + NoOp bool `json:"no_op"` + AlreadyClosed bool `json:"already_closed"` + AlreadyOpen bool `json:"already_open"` + AlreadyInGroup bool `json:"already_in_group"` + Channel Channel `json:"channel"` + History + SlackResponse +} + +func groupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*groupResponseFull, error) { + response := &groupResponseFull{} + err := postForm(ctx, client, SLACK_API+path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// ArchiveGroup archives a private group +func (api *Client) ArchiveGroup(group string) error { + return api.ArchiveGroupContext(context.Background(), group) +} + +// ArchiveGroupContext archives a private group +func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + } + + _, err := groupRequest(ctx, api.httpclient, "groups.archive", values, api.debug) + return err +} + +// UnarchiveGroup unarchives a private group +func (api *Client) UnarchiveGroup(group string) error { + return api.UnarchiveGroupContext(context.Background(), group) +} + +// UnarchiveGroupContext unarchives a private group +func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) error { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + } + + _, err := groupRequest(ctx, api.httpclient, "groups.unarchive", values, api.debug) + return err +} + +// CreateGroup creates a private group +func (api *Client) CreateGroup(group string) (*Group, error) { + return api.CreateGroupContext(context.Background(), group) +} + +// CreateGroupContext creates a private group +func (api *Client) CreateGroupContext(ctx context.Context, group string) (*Group, error) { + values := url.Values{ + "token": {api.token}, + "name": {group}, + } + + response, err := groupRequest(ctx, api.httpclient, "groups.create", values, api.debug) + if err != nil { + return nil, err + } + return &response.Group, nil +} + +// CreateChildGroup creates a new private group archiving the old one +// This method takes an existing private group and performs the following steps: +// 1. Renames the existing group (from "example" to "example-archived"). +// 2. Archives the existing group. +// 3. Creates a new group with the name of the existing group. +// 4. Adds all members of the existing group to the new group. +func (api *Client) CreateChildGroup(group string) (*Group, error) { + return api.CreateChildGroupContext(context.Background(), group) +} + +// 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.token}, + "channel": {group}, + } + + response, err := groupRequest(ctx, api.httpclient, "groups.createChild", values, api.debug) + if err != nil { + return nil, err + } + return &response.Group, nil +} + +// CloseGroup closes a private group +func (api *Client) CloseGroup(group string) (bool, bool, error) { + return api.CloseGroupContext(context.Background(), group) +} + +// CloseGroupContext closes a private group with a custom context +func (api *Client) CloseGroupContext(ctx context.Context, group string) (bool, bool, error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + } + + response, err := imRequest(ctx, api.httpclient, "groups.close", values, api.debug) + if err != nil { + return false, false, err + } + return response.NoOp, response.AlreadyClosed, nil +} + +// GetGroupHistory fetches all the history for a private group +func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*History, error) { + return api.GetGroupHistoryContext(context.Background(), group, params) +} + +// 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.token}, + "channel": {group}, + } + if params.Latest != DEFAULT_HISTORY_LATEST { + values.Add("latest", params.Latest) + } + if params.Oldest != DEFAULT_HISTORY_OLDEST { + values.Add("oldest", params.Oldest) + } + if params.Count != DEFAULT_HISTORY_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { + if params.Inclusive { + values.Add("inclusive", "1") + } else { + values.Add("inclusive", "0") + } + } + if params.Unreads != DEFAULT_HISTORY_UNREADS { + if params.Unreads { + values.Add("unreads", "1") + } else { + values.Add("unreads", "0") + } + } + + response, err := groupRequest(ctx, api.httpclient, "groups.history", values, api.debug) + if err != nil { + return nil, err + } + return &response.History, nil +} + +// InviteUserToGroup invites a specific user to a private group +func (api *Client) InviteUserToGroup(group, user string) (*Group, bool, error) { + return api.InviteUserToGroupContext(context.Background(), group, user) +} + +// 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.token}, + "channel": {group}, + "user": {user}, + } + + response, err := groupRequest(ctx, api.httpclient, "groups.invite", values, api.debug) + if err != nil { + return nil, false, err + } + return &response.Group, response.AlreadyInGroup, nil +} + +// LeaveGroup makes authenticated user leave the group +func (api *Client) LeaveGroup(group string) error { + return api.LeaveGroupContext(context.Background(), group) +} + +// LeaveGroupContext makes authenticated user leave the group with a custom context +func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + } + + _, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug) + return err +} + +// KickUserFromGroup kicks a user from a group +func (api *Client) KickUserFromGroup(group, user string) error { + return api.KickUserFromGroupContext(context.Background(), group, user) +} + +// KickUserFromGroupContext kicks a user from a group with a custom context +func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user string) (err error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + "user": {user}, + } + + _, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug) + return err +} + +// GetGroups retrieves all groups +func (api *Client) GetGroups(excludeArchived bool) ([]Group, error) { + return api.GetGroupsContext(context.Background(), excludeArchived) +} + +// GetGroupsContext retrieves all groups with a custom context +func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) ([]Group, error) { + values := url.Values{ + "token": {api.token}, + } + if excludeArchived { + values.Add("exclude_archived", "1") + } + + response, err := groupRequest(ctx, api.httpclient, "groups.list", values, api.debug) + if err != nil { + return nil, err + } + return response.Groups, nil +} + +// GetGroupInfo retrieves the given group +func (api *Client) GetGroupInfo(group string) (*Group, error) { + return api.GetGroupInfoContext(context.Background(), group) +} + +// GetGroupInfoContext retrieves the given group with a custom context +func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Group, error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + } + + response, err := groupRequest(ctx, api.httpclient, "groups.info", values, api.debug) + if err != nil { + return nil, err + } + return &response.Group, nil +} + +// SetGroupReadMark sets the read mark on a private group +// Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a +// 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) SetGroupReadMark(group, ts string) error { + return api.SetGroupReadMarkContext(context.Background(), group, ts) +} + +// 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) (err error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + "ts": {ts}, + } + + _, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug) + return err +} + +// OpenGroup opens a private group +func (api *Client) OpenGroup(group string) (bool, bool, error) { + return api.OpenGroupContext(context.Background(), group) +} + +// 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.token}, + "channel": {group}, + } + + response, err := groupRequest(ctx, api.httpclient, "groups.open", values, api.debug) + if err != nil { + return false, false, err + } + return response.NoOp, response.AlreadyOpen, nil +} + +// RenameGroup renames a group +// XXX: They return a channel, not a group. What is this crap? :( +// Inconsistent api it seems. +func (api *Client) RenameGroup(group, name string) (*Channel, error) { + return api.RenameGroupContext(context.Background(), group, name) +} + +// 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.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, api.httpclient, "groups.rename", values, api.debug) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// SetGroupPurpose sets the group purpose +func (api *Client) SetGroupPurpose(group, purpose string) (string, error) { + return api.SetGroupPurposeContext(context.Background(), group, purpose) +} + +// 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.token}, + "channel": {group}, + "purpose": {purpose}, + } + + response, err := groupRequest(ctx, api.httpclient, "groups.setPurpose", values, api.debug) + if err != nil { + return "", err + } + return response.Purpose, nil +} + +// SetGroupTopic sets the group topic +func (api *Client) SetGroupTopic(group, topic string) (string, error) { + return api.SetGroupTopicContext(context.Background(), group, topic) +} + +// 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.token}, + "channel": {group}, + "topic": {topic}, + } + + response, err := groupRequest(ctx, api.httpclient, "groups.setTopic", values, api.debug) + if err != nil { + return "", err + } + return response.Topic, nil +} diff --git a/vendor/github.com/nlopes/slack/history.go b/vendor/github.com/nlopes/slack/history.go new file mode 100644 index 0000000..87b2e1e --- /dev/null +++ b/vendor/github.com/nlopes/slack/history.go @@ -0,0 +1,36 @@ +package slack + +const ( + DEFAULT_HISTORY_LATEST = "" + DEFAULT_HISTORY_OLDEST = "0" + DEFAULT_HISTORY_COUNT = 100 + DEFAULT_HISTORY_INCLUSIVE = false + DEFAULT_HISTORY_UNREADS = false +) + +// HistoryParameters contains all the necessary information to help in the retrieval of history for Channels/Groups/DMs +type HistoryParameters struct { + Latest string + Oldest string + Count int + Inclusive bool + Unreads bool +} + +// History contains message history information needed to navigate a Channel / Group / DM history +type History struct { + Latest string `json:"latest"` + Messages []Message `json:"messages"` + HasMore bool `json:"has_more"` +} + +// NewHistoryParameters provides an instance of HistoryParameters with all the sane default values set +func NewHistoryParameters() HistoryParameters { + return HistoryParameters{ + Latest: DEFAULT_HISTORY_LATEST, + Oldest: DEFAULT_HISTORY_OLDEST, + Count: DEFAULT_HISTORY_COUNT, + Inclusive: DEFAULT_HISTORY_INCLUSIVE, + Unreads: DEFAULT_HISTORY_UNREADS, + } +} diff --git a/vendor/github.com/nlopes/slack/im.go b/vendor/github.com/nlopes/slack/im.go new file mode 100644 index 0000000..b6ffac3 --- /dev/null +++ b/vendor/github.com/nlopes/slack/im.go @@ -0,0 +1,159 @@ +package slack + +import ( + "context" + "errors" + "net/url" + "strconv" +) + +type imChannel struct { + ID string `json:"id"` +} + +type imResponseFull struct { + NoOp bool `json:"no_op"` + AlreadyClosed bool `json:"already_closed"` + AlreadyOpen bool `json:"already_open"` + Channel imChannel `json:"channel"` + IMs []IM `json:"ims"` + History + SlackResponse +} + +// IM contains information related to the Direct Message channel +type IM struct { + conversation + IsIM bool `json:"is_im"` + User string `json:"user"` + IsUserDeleted bool `json:"is_user_deleted"` +} + +func imRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*imResponseFull, error) { + response := &imResponseFull{} + err := postPath(ctx, client, path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// CloseIMChannel closes the direct message channel +func (api *Client) CloseIMChannel(channel string) (bool, bool, error) { + return api.CloseIMChannelContext(context.Background(), channel) +} + +// 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.token}, + "channel": {channel}, + } + + response, err := imRequest(ctx, api.httpclient, "im.close", values, api.debug) + if err != nil { + return false, false, err + } + return response.NoOp, response.AlreadyClosed, nil +} + +// OpenIMChannel opens a direct message channel to the user provided as argument +// Returns some status and the channel ID +func (api *Client) OpenIMChannel(user string) (bool, bool, string, error) { + return api.OpenIMChannelContext(context.Background(), user) +} + +// OpenIMChannelContext opens a direct message channel to the user provided as argument with a custom context +// 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.token}, + "user": {user}, + } + + response, err := imRequest(ctx, api.httpclient, "im.open", values, api.debug) + if err != nil { + return false, false, "", err + } + return response.NoOp, response.AlreadyOpen, response.Channel.ID, nil +} + +// MarkIMChannel sets the read mark of a direct message channel to a specific point +func (api *Client) MarkIMChannel(channel, ts string) (err error) { + return api.MarkIMChannelContext(context.Background(), channel, ts) +} + +// 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) error { + values := url.Values{ + "token": {api.token}, + "channel": {channel}, + "ts": {ts}, + } + + _, err := imRequest(ctx, api.httpclient, "im.mark", values, api.debug) + return err +} + +// GetIMHistory retrieves the direct message channel history +func (api *Client) GetIMHistory(channel string, params HistoryParameters) (*History, error) { + return api.GetIMHistoryContext(context.Background(), channel, params) +} + +// 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.token}, + "channel": {channel}, + } + if params.Latest != DEFAULT_HISTORY_LATEST { + values.Add("latest", params.Latest) + } + if params.Oldest != DEFAULT_HISTORY_OLDEST { + values.Add("oldest", params.Oldest) + } + if params.Count != DEFAULT_HISTORY_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { + if params.Inclusive { + values.Add("inclusive", "1") + } else { + values.Add("inclusive", "0") + } + } + if params.Unreads != DEFAULT_HISTORY_UNREADS { + if params.Unreads { + values.Add("unreads", "1") + } else { + values.Add("unreads", "0") + } + } + + response, err := imRequest(ctx, api.httpclient, "im.history", values, api.debug) + if err != nil { + return nil, err + } + return &response.History, nil +} + +// GetIMChannels returns the list of direct message channels +func (api *Client) GetIMChannels() ([]IM, error) { + return api.GetIMChannelsContext(context.Background()) +} + +// 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.token}, + } + + response, err := imRequest(ctx, api.httpclient, "im.list", values, api.debug) + if err != nil { + return nil, err + } + return response.IMs, nil +} diff --git a/vendor/github.com/nlopes/slack/info.go b/vendor/github.com/nlopes/slack/info.go new file mode 100644 index 0000000..db8534c --- /dev/null +++ b/vendor/github.com/nlopes/slack/info.go @@ -0,0 +1,225 @@ +package slack + +import ( + "bytes" + "fmt" + "strconv" + "time" +) + +// UserPrefs needs to be implemented +type UserPrefs struct { + // "highlight_words":"", + // "user_colors":"", + // "color_names_in_list":true, + // "growls_enabled":true, + // "tz":"Europe\/London", + // "push_dm_alert":true, + // "push_mention_alert":true, + // "push_everything":true, + // "push_idle_wait":2, + // "push_sound":"b2.mp3", + // "push_loud_channels":"", + // "push_mention_channels":"", + // "push_loud_channels_set":"", + // "email_alerts":"instant", + // "email_alerts_sleep_until":0, + // "email_misc":false, + // "email_weekly":true, + // "welcome_message_hidden":false, + // "all_channels_loud":true, + // "loud_channels":"", + // "never_channels":"", + // "loud_channels_set":"", + // "show_member_presence":true, + // "search_sort":"timestamp", + // "expand_inline_imgs":true, + // "expand_internal_inline_imgs":true, + // "expand_snippets":false, + // "posts_formatting_guide":true, + // "seen_welcome_2":true, + // "seen_ssb_prompt":false, + // "search_only_my_channels":false, + // "emoji_mode":"default", + // "has_invited":true, + // "has_uploaded":false, + // "has_created_channel":true, + // "search_exclude_channels":"", + // "messages_theme":"default", + // "webapp_spellcheck":true, + // "no_joined_overlays":false, + // "no_created_overlays":true, + // "dropbox_enabled":false, + // "seen_user_menu_tip_card":true, + // "seen_team_menu_tip_card":true, + // "seen_channel_menu_tip_card":true, + // "seen_message_input_tip_card":true, + // "seen_channels_tip_card":true, + // "seen_domain_invite_reminder":false, + // "seen_member_invite_reminder":false, + // "seen_flexpane_tip_card":true, + // "seen_search_input_tip_card":true, + // "mute_sounds":false, + // "arrow_history":false, + // "tab_ui_return_selects":true, + // "obey_inline_img_limit":true, + // "new_msg_snd":"knock_brush.mp3", + // "collapsible":false, + // "collapsible_by_click":true, + // "require_at":false, + // "mac_ssb_bounce":"", + // "mac_ssb_bullet":true, + // "win_ssb_bullet":true, + // "expand_non_media_attachments":true, + // "show_typing":true, + // "pagekeys_handled":true, + // "last_snippet_type":"", + // "display_real_names_override":0, + // "time24":false, + // "enter_is_special_in_tbt":false, + // "graphic_emoticons":false, + // "convert_emoticons":true, + // "autoplay_chat_sounds":true, + // "ss_emojis":true, + // "sidebar_behavior":"", + // "mark_msgs_read_immediately":true, + // "start_scroll_at_oldest":true, + // "snippet_editor_wrap_long_lines":false, + // "ls_disabled":false, + // "sidebar_theme":"default", + // "sidebar_theme_custom_values":"", + // "f_key_search":false, + // "k_key_omnibox":true, + // "speak_growls":false, + // "mac_speak_voice":"com.apple.speech.synthesis.voice.Alex", + // "mac_speak_speed":250, + // "comma_key_prefs":false, + // "at_channel_suppressed_channels":"", + // "push_at_channel_suppressed_channels":"", + // "prompted_for_email_disabling":false, + // "full_text_extracts":false, + // "no_text_in_notifications":false, + // "muted_channels":"", + // "no_macssb1_banner":false, + // "privacy_policy_seen":true, + // "search_exclude_bots":false, + // "fuzzy_matching":false +} + +// UserDetails contains user details coming in the initial response from StartRTM +type UserDetails struct { + ID string `json:"id"` + Name string `json:"name"` + Created JSONTime `json:"created"` + ManualPresence string `json:"manual_presence"` + Prefs UserPrefs `json:"prefs"` +} + +// JSONTime exists so that we can have a String method converting the date +type JSONTime int64 + +// String converts the unix timestamp into a string +func (t JSONTime) String() string { + tm := t.Time() + return fmt.Sprintf("\"%s\"", tm.Format("Mon Jan _2")) +} + +// Time returns a `time.Time` representation of this value. +func (t JSONTime) Time() time.Time { + return time.Unix(int64(t), 0) +} + +// UnmarshalJSON will unmarshal both string and int JSON values +func (t *JSONTime) UnmarshalJSON(buf []byte) error { + s := bytes.Trim(buf, `"`) + + v, err := strconv.Atoi(string(s)) + if err != nil { + return err + } + + *t = JSONTime(int64(v)) + return nil +} + +// Team contains details about a team +type Team struct { + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` +} + +// Icons XXX: needs further investigation +type Icons struct { + Image36 string `json:"image_36,omitempty"` + Image48 string `json:"image_48,omitempty"` + Image72 string `json:"image_72,omitempty"` +} + +// Info contains various details about Users, Channels, Bots and the authenticated user. +// It is returned by StartRTM or included in the "ConnectedEvent" RTM event. +type Info struct { + URL string `json:"url,omitempty"` + User *UserDetails `json:"self,omitempty"` + Team *Team `json:"team,omitempty"` + Users []User `json:"users,omitempty"` + Channels []Channel `json:"channels,omitempty"` + Groups []Group `json:"groups,omitempty"` + Bots []Bot `json:"bots,omitempty"` + IMs []IM `json:"ims,omitempty"` +} + +type infoResponseFull struct { + Info + SlackResponse +} + +// GetBotByID returns a bot given a bot id +func (info Info) GetBotByID(botID string) *Bot { + for _, bot := range info.Bots { + if bot.ID == botID { + return &bot + } + } + return nil +} + +// GetUserByID returns a user given a user id +func (info Info) GetUserByID(userID string) *User { + for _, user := range info.Users { + if user.ID == userID { + return &user + } + } + return nil +} + +// GetChannelByID returns a channel given a channel id +func (info Info) GetChannelByID(channelID string) *Channel { + for _, channel := range info.Channels { + if channel.ID == channelID { + return &channel + } + } + return nil +} + +// GetGroupByID returns a group given a group id +func (info Info) GetGroupByID(groupID string) *Group { + for _, group := range info.Groups { + if group.ID == groupID { + return &group + } + } + return nil +} + +// GetIMByID returns an IM given an IM id +func (info Info) GetIMByID(imID string) *IM { + for _, im := range info.IMs { + if im.ID == imID { + return &im + } + } + return nil +} diff --git a/vendor/github.com/nlopes/slack/item.go b/vendor/github.com/nlopes/slack/item.go new file mode 100644 index 0000000..89af4eb --- /dev/null +++ b/vendor/github.com/nlopes/slack/item.go @@ -0,0 +1,75 @@ +package slack + +const ( + TYPE_MESSAGE = "message" + TYPE_FILE = "file" + TYPE_FILE_COMMENT = "file_comment" + TYPE_CHANNEL = "channel" + TYPE_IM = "im" + TYPE_GROUP = "group" +) + +// Item is any type of slack message - message, file, or file comment. +type Item struct { + Type string `json:"type"` + Channel string `json:"channel,omitempty"` + Message *Message `json:"message,omitempty"` + File *File `json:"file,omitempty"` + Comment *Comment `json:"comment,omitempty"` + Timestamp string `json:"ts,omitempty"` +} + +// NewMessageItem turns a message on a channel into a typed message struct. +func NewMessageItem(ch string, m *Message) Item { + return Item{Type: TYPE_MESSAGE, Channel: ch, Message: m} +} + +// NewFileItem turns a file into a typed file struct. +func NewFileItem(f *File) Item { + return Item{Type: TYPE_FILE, File: f} +} + +// NewFileCommentItem turns a file and comment into a typed file_comment struct. +func NewFileCommentItem(f *File, c *Comment) Item { + return Item{Type: TYPE_FILE_COMMENT, File: f, Comment: c} +} + +// NewChannelItem turns a channel id into a typed channel struct. +func NewChannelItem(ch string) Item { + return Item{Type: TYPE_CHANNEL, Channel: ch} +} + +// NewIMItem turns a channel id into a typed im struct. +func NewIMItem(ch string) Item { + return Item{Type: TYPE_IM, Channel: ch} +} + +// NewGroupItem turns a channel id into a typed group struct. +func NewGroupItem(ch string) Item { + return Item{Type: TYPE_GROUP, Channel: ch} +} + +// ItemRef is a reference to a message of any type. One of FileID, +// CommentId, or the combination of ChannelId and Timestamp must be +// specified. +type ItemRef struct { + Channel string `json:"channel"` + Timestamp string `json:"timestamp"` + File string `json:"file"` + Comment string `json:"file_comment"` +} + +// NewRefToMessage initializes a reference to to a message. +func NewRefToMessage(channel, timestamp string) ItemRef { + return ItemRef{Channel: channel, Timestamp: timestamp} +} + +// NewRefToFile initializes a reference to a file. +func NewRefToFile(file string) ItemRef { + return ItemRef{File: file} +} + +// NewRefToComment initializes a reference to a file comment. +func NewRefToComment(comment string) ItemRef { + return ItemRef{Comment: comment} +} 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/messageID.go b/vendor/github.com/nlopes/slack/messageID.go new file mode 100644 index 0000000..a17472b --- /dev/null +++ b/vendor/github.com/nlopes/slack/messageID.go @@ -0,0 +1,30 @@ +package slack + +import "sync" + +// IDGenerator provides an interface for generating integer ID values. +type IDGenerator interface { + Next() int +} + +// NewSafeID returns a new instance of an IDGenerator which is safe for +// concurrent use by multiple goroutines. +func NewSafeID(startID int) IDGenerator { + return &safeID{ + nextID: startID, + mutex: &sync.Mutex{}, + } +} + +type safeID struct { + nextID int + mutex *sync.Mutex +} + +func (s *safeID) Next() int { + s.mutex.Lock() + defer s.mutex.Unlock() + id := s.nextID + s.nextID++ + return id +} diff --git a/vendor/github.com/nlopes/slack/messages.go b/vendor/github.com/nlopes/slack/messages.go new file mode 100644 index 0000000..a21e0ef --- /dev/null +++ b/vendor/github.com/nlopes/slack/messages.go @@ -0,0 +1,178 @@ +package slack + +// OutgoingMessage is used for the realtime API, and seems incomplete. +type OutgoingMessage struct { + ID int `json:"id"` + // channel ID + Channel string `json:"channel,omitempty"` + Text string `json:"text,omitempty"` + Type string `json:"type,omitempty"` + ThreadTimestamp string `json:"thread_ts,omitempty"` + ThreadBroadcast bool `json:"reply_broadcast,omitempty"` +} + +// Message is an auxiliary type to allow us to have a message containing sub messages +type Message struct { + Msg + SubMessage *Msg `json:"message,omitempty"` +} + +// Msg contains information about a slack message +type Msg struct { + // Basic Message + Type string `json:"type,omitempty"` + Channel string `json:"channel,omitempty"` + User string `json:"user,omitempty"` + Text string `json:"text,omitempty"` + Timestamp string `json:"ts,omitempty"` + ThreadTimestamp string `json:"thread_ts,omitempty"` + IsStarred bool `json:"is_starred,omitempty"` + 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"` + + // Hidden Subtypes + Hidden bool `json:"hidden,omitempty"` // message_changed, message_deleted, unpinned_item + DeletedTimestamp string `json:"deleted_ts,omitempty"` // message_deleted + EventTimestamp string `json:"event_ts,omitempty"` + + // bot_message (https://api.slack.com/events/message/bot_message) + BotID string `json:"bot_id,omitempty"` + Username string `json:"username,omitempty"` + Icons *Icon `json:"icons,omitempty"` + + // channel_join, group_join + Inviter string `json:"inviter,omitempty"` + + // channel_topic, group_topic + Topic string `json:"topic,omitempty"` + + // channel_purpose, group_purpose + Purpose string `json:"purpose,omitempty"` + + // channel_name, group_name + Name string `json:"name,omitempty"` + OldName string `json:"old_name,omitempty"` + + // channel_archive, group_archive + Members []string `json:"members,omitempty"` + + // channels.replies, groups.replies, im.replies, mpim.replies + ReplyCount int `json:"reply_count,omitempty"` + Replies []Reply `json:"replies,omitempty"` + ParentUserId string `json:"parent_user_id,omitempty"` + + // file_share, file_comment, file_mention + File *File `json:"file,omitempty"` + + // file_share + Upload bool `json:"upload,omitempty"` + + // file_comment + Comment *Comment `json:"comment,omitempty"` + + // pinned_item + ItemType string `json:"item_type,omitempty"` + + // https://api.slack.com/rtm + ReplyTo int `json:"reply_to,omitempty"` + Team string `json:"team,omitempty"` + + // reactions + Reactions []ItemReaction `json:"reactions,omitempty"` + + // slash commands and interactive messages + ResponseType string `json:"response_type,omitempty"` + ReplaceOriginal bool `json:"replace_original,omitempty"` + DeleteOriginal bool `json:"delete_original,omitempty"` +} + +// Icon is used for bot messages +type Icon struct { + IconURL string `json:"icon_url,omitempty"` + IconEmoji string `json:"icon_emoji,omitempty"` +} + +// Edited indicates that a message has been edited. +type Edited struct { + User string `json:"user,omitempty"` + Timestamp string `json:"ts,omitempty"` +} + +// Reply contains information about a reply for a thread +type Reply struct { + User string `json:"user,omitempty"` + Timestamp string `json:"ts,omitempty"` +} + +// Event contains the event type +type Event struct { + Type string `json:"type,omitempty"` +} + +// Ping contains information about a Ping Event +type Ping struct { + ID int `json:"id"` + Type string `json:"type"` + Timestamp int64 `json:"timestamp"` +} + +// Pong contains information about a Pong Event +type Pong struct { + Type string `json:"type"` + ReplyTo int `json:"reply_to"` + Timestamp int64 `json:"timestamp"` +} + +// 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, channelID string, options ...RTMsgOption) *OutgoingMessage { + id := rtm.idGen.Next() + msg := OutgoingMessage{ + ID: id, + Type: "message", + Channel: channelID, + Text: text, + } + for _, option := range options { + option(&msg) + } + return &msg +} + +// 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(channelID string) *OutgoingMessage { + id := rtm.idGen.Next() + return &OutgoingMessage{ + ID: id, + Type: "typing", + Channel: channelID, + } +} + +// RTMsgOption allows configuration of various options available for sending an RTM message +type RTMsgOption func(*OutgoingMessage) + +// RTMsgOptionTS sets thead timestamp of an outgoing message in order to respond to a thread +func RTMsgOptionTS(threadTimestamp string) RTMsgOption { + return func(msg *OutgoingMessage) { + msg.ThreadTimestamp = threadTimestamp + } +} + +// RTMsgOptionBroadcast sets broadcast reply to channel to "true" +func RTMsgOptionBroadcast() RTMsgOption { + return func(msg *OutgoingMessage) { + msg.ThreadBroadcast = true + } + +} diff --git a/vendor/github.com/nlopes/slack/misc.go b/vendor/github.com/nlopes/slack/misc.go new file mode 100644 index 0000000..8cf56ba --- /dev/null +++ b/vendor/github.com/nlopes/slack/misc.go @@ -0,0 +1,237 @@ +package slack + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "net/http/httputil" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "time" +) + +type SlackResponse struct { + Ok bool `json:"ok"` + Error string `json:"error"` +} + +func (t SlackResponse) Err() error { + if t.Ok { + return nil + } + + // handle pure text based responses like chat.post + // which while they have a slack response in their data structure + // it doesn't actually get set during parsing. + if strings.TrimSpace(t.Error) == "" { + return nil + } + + return errors.New(t.Error) +} + +// StatusCodeError represents an http response error. +// type httpStatusCode interface { HTTPStatusCode() int } to handle it. +type statusCodeError struct { + Code int + Status string +} + +func (t statusCodeError) Error() string { + // TODO: this is a bad error string, should clean it up with a breaking changes + // merger. + return fmt.Sprintf("Slack server error: %s.", t.Status) +} + +func (t statusCodeError) HTTPStatusCode() int { + return t.Code +} + +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) + + ioWriter, err := wr.CreateFormFile(fieldname, filename) + if err != nil { + wr.Close() + return nil, err + } + _, err = io.Copy(ioWriter, r) + if err != nil { + wr.Close() + return nil, err + } + // Close the multipart writer or the footer won't be written + wr.Close() + req, err := http.NewRequest("POST", path, body) + req = req.WithContext(ctx) + if err != nil { + return nil, err + } + req.Header.Add("Content-Type", wr.FormDataContentType()) + req.URL.RawQuery = (values).Encode() + return req, nil +} + +func parseResponseBody(body io.ReadCloser, intf interface{}, debug bool) error { + response, err := ioutil.ReadAll(body) + if err != nil { + return err + } + + // FIXME: will be api.Debugf + if debug { + logger.Printf("parseResponseBody: %s\n", string(response)) + } + + return json.Unmarshal(response, intf) +} + +func postLocalWithMultipartResponse(ctx context.Context, client HTTPRequester, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error { + fullpath, err := filepath.Abs(fpath) + if err != nil { + return err + } + file, err := os.Open(fullpath) + if err != nil { + return err + } + defer file.Close() + return postWithMultipartResponse(ctx, client, path, filepath.Base(fpath), fieldname, values, file, intf, debug) +} + +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 := 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 != http.StatusOK { + logResponse(resp, debug) + return statusCodeError{Code: resp.StatusCode, Status: resp.Status} + } + + return parseResponseBody(resp.Body, intf, debug) +} + +func doPost(ctx context.Context, client HTTPRequester, req *http.Request, intf interface{}, debug bool) error { + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusTooManyRequests { + retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64) + if err != nil { + return err + } + return &RateLimitedError{time.Duration(retry) * time.Second} + } + + // Slack seems to send an HTML body along with 5xx error codes. Don't parse it. + if resp.StatusCode != http.StatusOK { + logResponse(resp, debug) + return statusCodeError{Code: resp.StatusCode, Status: resp.Status} + } + + return parseResponseBody(resp.Body, intf, debug) +} + +func postJSON(ctx context.Context, client HTTPRequester, endpoint, token string, json []byte, intf interface{}, debug bool) error { + reqBody := bytes.NewBuffer(json) + req, err := http.NewRequest("POST", endpoint, reqBody) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + return doPost(ctx, client, req, intf, debug) +} + +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 { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + return doPost(ctx, client, req, intf, debug) +} + +func postPath(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, client HTTPRequester, method string, teamName string, values url.Values, intf interface{}, debug bool) error { + endpoint := fmt.Sprintf(SLACK_WEB_API_FORMAT, teamName, method, time.Now().Unix()) + return postForm(ctx, client, endpoint, values, intf, debug) +} + +func logResponse(resp *http.Response, debug bool) error { + if debug { + text, err := httputil.DumpResponse(resp, true) + if err != nil { + return err + } + + logger.Print(string(text)) + } + + return nil +} + +func okJsonHandler(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + response, _ := json.Marshal(SlackResponse{ + Ok: true, + }) + rw.Write(response) +} + +type errorString string + +func (t errorString) Error() string { + return string(t) +} + +// timerReset safely reset a timer, see time.Timer.Reset for details. +func timerReset(t *time.Timer, d time.Duration) { + if !t.Stop() { + <-t.C + } + t.Reset(d) +} diff --git a/vendor/github.com/nlopes/slack/oauth.go b/vendor/github.com/nlopes/slack/oauth.go new file mode 100644 index 0000000..498e28d --- /dev/null +++ b/vendor/github.com/nlopes/slack/oauth.go @@ -0,0 +1,66 @@ +package slack + +import ( + "context" + "errors" + "net/url" +) + +type OAuthResponseIncomingWebhook struct { + URL string `json:"url"` + Channel string `json:"channel"` + ChannelID string `json:"channel_id,omitempty"` + ConfigurationURL string `json:"configuration_url"` +} + +type OAuthResponseBot struct { + BotUserID string `json:"bot_user_id"` + BotAccessToken string `json:"bot_access_token"` +} + +type OAuthResponse struct { + AccessToken string `json:"access_token"` + Scope string `json:"scope"` + TeamName string `json:"team_name"` + TeamID string `json:"team_id"` + IncomingWebhook OAuthResponseIncomingWebhook `json:"incoming_webhook"` + Bot OAuthResponseBot `json:"bot"` + UserID string `json:"user_id,omitempty"` + SlackResponse +} + +// GetOAuthToken retrieves an AccessToken +func GetOAuthToken(clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) { + return GetOAuthTokenContext(context.Background(), clientID, clientSecret, code, redirectURI, debug) +} + +// GetOAuthTokenContext retrieves an AccessToken with a custom context +func GetOAuthTokenContext(ctx context.Context, clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) { + response, err := GetOAuthResponseContext(ctx, clientID, clientSecret, code, redirectURI, debug) + if err != nil { + return "", "", err + } + return response.AccessToken, response.Scope, nil +} + +func GetOAuthResponse(clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) { + return GetOAuthResponseContext(context.Background(), clientID, clientSecret, code, redirectURI, debug) +} + +func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) { + values := url.Values{ + "client_id": {clientID}, + "client_secret": {clientSecret}, + "code": {code}, + "redirect_uri": {redirectURI}, + } + response := &OAuthResponse{} + err = postPath(ctx, customHTTPClient, "oauth.access", values, response, 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/pagination.go b/vendor/github.com/nlopes/slack/pagination.go new file mode 100644 index 0000000..87dd136 --- /dev/null +++ b/vendor/github.com/nlopes/slack/pagination.go @@ -0,0 +1,20 @@ +package slack + +// Paging contains paging information +type Paging struct { + Count int `json:"count"` + Total int `json:"total"` + Page int `json:"page"` + Pages int `json:"pages"` +} + +// Pagination contains pagination information +// This is different from Paging in that it contains additional details +type Pagination struct { + TotalCount int `json:"total_count"` + Page int `json:"page"` + PerPage int `json:"per_page"` + PageCount int `json:"page_count"` + First int `json:"first"` + Last int `json:"last"` +} diff --git a/vendor/github.com/nlopes/slack/pins.go b/vendor/github.com/nlopes/slack/pins.go new file mode 100644 index 0000000..7d95f74 --- /dev/null +++ b/vendor/github.com/nlopes/slack/pins.go @@ -0,0 +1,94 @@ +package slack + +import ( + "context" + "errors" + "net/url" +) + +type listPinsResponseFull struct { + Items []Item + Paging `json:"paging"` + SlackResponse +} + +// AddPin pins an item in a channel +func (api *Client) AddPin(channel string, item ItemRef) error { + return api.AddPinContext(context.Background(), channel, item) +} + +// AddPinContext pins an item in a channel with a custom context +func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemRef) error { + values := url.Values{ + "channel": {channel}, + "token": {api.token}, + } + if item.Timestamp != "" { + values.Set("timestamp", item.Timestamp) + } + if item.File != "" { + values.Set("file", item.File) + } + if item.Comment != "" { + values.Set("file_comment", item.Comment) + } + + response := &SlackResponse{} + if err := postPath(ctx, api.httpclient, "pins.add", values, response, api.debug); err != nil { + return err + } + + return response.Err() +} + +// RemovePin un-pins an item from a channel +func (api *Client) RemovePin(channel string, item ItemRef) error { + return api.RemovePinContext(context.Background(), channel, item) +} + +// RemovePinContext un-pins an item from a channel with a custom context +func (api *Client) RemovePinContext(ctx context.Context, channel string, item ItemRef) error { + values := url.Values{ + "channel": {channel}, + "token": {api.token}, + } + if item.Timestamp != "" { + values.Set("timestamp", item.Timestamp) + } + if item.File != "" { + values.Set("file", item.File) + } + if item.Comment != "" { + values.Set("file_comment", item.Comment) + } + + response := &SlackResponse{} + if err := postPath(ctx, api.httpclient, "pins.remove", values, response, api.debug); err != nil { + return err + } + + return response.Err() +} + +// ListPins returns information about the items a user reacted to. +func (api *Client) ListPins(channel string) ([]Item, *Paging, error) { + return api.ListPinsContext(context.Background(), channel) +} + +// ListPinsContext returns information about the items a user reacted to with a custom context. +func (api *Client) ListPinsContext(ctx context.Context, channel string) ([]Item, *Paging, error) { + values := url.Values{ + "channel": {channel}, + "token": {api.token}, + } + + response := &listPinsResponseFull{} + err := postPath(ctx, api.httpclient, "pins.list", values, response, api.debug) + if err != nil { + return nil, nil, err + } + if !response.Ok { + return nil, nil, errors.New(response.Error) + } + return response.Items, &response.Paging, nil +} diff --git a/vendor/github.com/nlopes/slack/reactions.go b/vendor/github.com/nlopes/slack/reactions.go new file mode 100644 index 0000000..50774f2 --- /dev/null +++ b/vendor/github.com/nlopes/slack/reactions.go @@ -0,0 +1,267 @@ +package slack + +import ( + "context" + "errors" + "net/url" + "strconv" +) + +// ItemReaction is the reactions that have happened on an item. +type ItemReaction struct { + Name string `json:"name"` + Count int `json:"count"` + Users []string `json:"users"` +} + +// ReactedItem is an item that was reacted to, and the details of the +// reactions. +type ReactedItem struct { + Item + Reactions []ItemReaction +} + +// GetReactionsParameters is the inputs to get reactions to an item. +type GetReactionsParameters struct { + Full bool +} + +// NewGetReactionsParameters initializes the inputs to get reactions to an item. +func NewGetReactionsParameters() GetReactionsParameters { + return GetReactionsParameters{ + Full: false, + } +} + +type getReactionsResponseFull struct { + Type string + M struct { + Reactions []ItemReaction + } `json:"message"` + F struct { + Reactions []ItemReaction + } `json:"file"` + FC struct { + Reactions []ItemReaction + } `json:"comment"` + SlackResponse +} + +func (res getReactionsResponseFull) extractReactions() []ItemReaction { + switch res.Type { + case "message": + return res.M.Reactions + case "file": + return res.F.Reactions + case "file_comment": + return res.FC.Reactions + } + return []ItemReaction{} +} + +const ( + DEFAULT_REACTIONS_USER = "" + DEFAULT_REACTIONS_COUNT = 100 + DEFAULT_REACTIONS_PAGE = 1 + DEFAULT_REACTIONS_FULL = false +) + +// ListReactionsParameters is the inputs to find all reactions by a user. +type ListReactionsParameters struct { + User string + Count int + Page int + Full bool +} + +// NewListReactionsParameters initializes the inputs to find all reactions +// performed by a user. +func NewListReactionsParameters() ListReactionsParameters { + return ListReactionsParameters{ + User: DEFAULT_REACTIONS_USER, + Count: DEFAULT_REACTIONS_COUNT, + Page: DEFAULT_REACTIONS_PAGE, + Full: DEFAULT_REACTIONS_FULL, + } +} + +type listReactionsResponseFull struct { + Items []struct { + Type string + Channel string + M struct { + *Message + } `json:"message"` + F struct { + *File + Reactions []ItemReaction + } `json:"file"` + FC struct { + *Comment + Reactions []ItemReaction + } `json:"comment"` + } + Paging `json:"paging"` + SlackResponse +} + +func (res listReactionsResponseFull) extractReactedItems() []ReactedItem { + items := make([]ReactedItem, len(res.Items)) + for i, input := range res.Items { + item := ReactedItem{} + item.Type = input.Type + switch input.Type { + case "message": + item.Channel = input.Channel + item.Message = input.M.Message + item.Reactions = input.M.Reactions + case "file": + item.File = input.F.File + item.Reactions = input.F.Reactions + case "file_comment": + item.File = input.F.File + item.Comment = input.FC.Comment + item.Reactions = input.FC.Reactions + } + items[i] = item + } + return items +} + +// AddReaction adds a reaction emoji to a message, file or file comment. +func (api *Client) AddReaction(name string, item ItemRef) error { + return api.AddReactionContext(context.Background(), name, item) +} + +// 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.token}, + } + if name != "" { + values.Set("name", name) + } + if item.Channel != "" { + values.Set("channel", item.Channel) + } + if item.Timestamp != "" { + values.Set("timestamp", item.Timestamp) + } + if item.File != "" { + values.Set("file", item.File) + } + if item.Comment != "" { + values.Set("file_comment", item.Comment) + } + + response := &SlackResponse{} + if err := postPath(ctx, api.httpclient, "reactions.add", values, response, api.debug); err != nil { + return err + } + + return response.Err() +} + +// RemoveReaction removes a reaction emoji from a message, file or file comment. +func (api *Client) RemoveReaction(name string, item ItemRef) error { + return api.RemoveReactionContext(context.Background(), name, item) +} + +// 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.token}, + } + if name != "" { + values.Set("name", name) + } + if item.Channel != "" { + values.Set("channel", item.Channel) + } + if item.Timestamp != "" { + values.Set("timestamp", item.Timestamp) + } + if item.File != "" { + values.Set("file", item.File) + } + if item.Comment != "" { + values.Set("file_comment", item.Comment) + } + + response := &SlackResponse{} + if err := postPath(ctx, api.httpclient, "reactions.remove", values, response, api.debug); err != nil { + return err + } + + return response.Err() +} + +// GetReactions returns details about the reactions on an item. +func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) { + return api.GetReactionsContext(context.Background(), item, params) +} + +// 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.token}, + } + if item.Channel != "" { + values.Set("channel", item.Channel) + } + if item.Timestamp != "" { + values.Set("timestamp", item.Timestamp) + } + if item.File != "" { + values.Set("file", item.File) + } + if item.Comment != "" { + values.Set("file_comment", item.Comment) + } + if params.Full != DEFAULT_REACTIONS_FULL { + values.Set("full", strconv.FormatBool(params.Full)) + } + + response := &getReactionsResponseFull{} + if err := postPath(ctx, api.httpclient, "reactions.get", values, response, api.debug); err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response.extractReactions(), nil +} + +// ListReactions returns information about the items a user reacted to. +func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, *Paging, error) { + return api.ListReactionsContext(context.Background(), params) +} + +// 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.token}, + } + if params.User != DEFAULT_REACTIONS_USER { + values.Add("user", params.User) + } + if params.Count != DEFAULT_REACTIONS_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Page != DEFAULT_REACTIONS_PAGE { + values.Add("page", strconv.Itoa(params.Page)) + } + if params.Full != DEFAULT_REACTIONS_FULL { + values.Add("full", strconv.FormatBool(params.Full)) + } + + response := &listReactionsResponseFull{} + err := postPath(ctx, api.httpclient, "reactions.list", values, response, api.debug) + if err != nil { + return nil, nil, err + } + if !response.Ok { + return nil, nil, errors.New(response.Error) + } + return response.extractReactedItems(), &response.Paging, nil +} diff --git a/vendor/github.com/nlopes/slack/rtm.go b/vendor/github.com/nlopes/slack/rtm.go new file mode 100644 index 0000000..e2c2b8e --- /dev/null +++ b/vendor/github.com/nlopes/slack/rtm.go @@ -0,0 +1,138 @@ +package slack + +import ( + "context" + "encoding/json" + "net/url" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +const ( + websocketDefaultTimeout = 10 * time.Second + defaultPingInterval = 30 * time.Second +) + +const ( + rtmEventTypeAck = "" + rtmEventTypeHello = "hello" + rtmEventTypeGoodbye = "goodbye" + rtmEventTypePong = "pong" + rtmEventTypeDesktopNotification = "desktop_notification" +) + +// 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) { + 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. +// +// 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 = postPath(ctx, api.httpclient, "rtm.start", url.Values{"token": {api.token}}, response, api.debug) + if err != nil { + return nil, "", err + } + + api.Debugln("Using URL:", response.Info.URL) + return &response.Info, response.Info.URL, response.Err() +} + +// 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) { + ctx, cancel := context.WithTimeout(context.Background(), websocketDefaultTimeout) + defer cancel() + + return api.ConnectRTMContext(ctx) +} + +// ConnectRTMContext calls the "rtm.connect" endpoint and returns the +// provided URL and the compact Info block with a custom context. +// +// 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 = postPath(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, "", err + } + + api.Debugln("Using URL:", response.Info.URL) + return &response.Info, response.Info.URL, response.Err() +} + +// RTMOption options for the managed RTM. +type RTMOption func(*RTM) + +// RTMOptionUseStart as of 11th July 2017 you should prefer setting this to false, see: +// https://api.slack.com/changelog/2017-04-start-using-rtm-connect-and-stop-using-rtm-start +func RTMOptionUseStart(b bool) RTMOption { + return func(rtm *RTM) { + rtm.useRTMStart = b + } +} + +// RTMOptionDialer takes a gorilla websocket Dialer and uses it as the +// Dialer when opening the websocket for the RTM connection. +func RTMOptionDialer(d *websocket.Dialer) RTMOption { + return func(rtm *RTM) { + rtm.dialer = d + } +} + +// RTMOptionPingInterval determines how often to deliver a ping message to slack. +func RTMOptionPingInterval(d time.Duration) RTMOption { + return func(rtm *RTM) { + rtm.pingInterval = d + rtm.resetDeadman() + } +} + +// NewRTM returns a RTM, which provides a fully managed connection to +// Slack's websocket-based Real-Time Messaging protocol. +func (api *Client) NewRTM(options ...RTMOption) *RTM { + result := &RTM{ + Client: *api, + IncomingEvents: make(chan RTMEvent, 50), + outgoingMessages: make(chan OutgoingMessage, 20), + pingInterval: defaultPingInterval, + pingDeadman: time.NewTimer(deadmanDuration(defaultPingInterval)), + isConnected: false, + wasIntentional: true, + killChannel: make(chan bool), + disconnected: make(chan struct{}), + forcePing: make(chan bool), + rawEvents: make(chan json.RawMessage), + idGen: NewSafeID(1), + mu: &sync.Mutex{}, + } + + for _, opt := range options { + opt(result) + } + + return result +} + +// NewRTMWithOptions Deprecated just use NewRTM(RTMOptionsUseStart(true)) +// returns a RTM, which provides a fully managed connection to +// Slack's websocket-based Real-Time Messaging protocol. +// This also allows to configure various options available for RTM API. +func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM { + if options != nil { + return api.NewRTM(RTMOptionUseStart(options.UseRTMStart)) + } + return api.NewRTM() +} diff --git a/vendor/github.com/nlopes/slack/search.go b/vendor/github.com/nlopes/slack/search.go new file mode 100644 index 0000000..1039637 --- /dev/null +++ b/vendor/github.com/nlopes/slack/search.go @@ -0,0 +1,152 @@ +package slack + +import ( + "context" + "errors" + "net/url" + "strconv" +) + +const ( + DEFAULT_SEARCH_SORT = "score" + DEFAULT_SEARCH_SORT_DIR = "desc" + DEFAULT_SEARCH_HIGHLIGHT = false + DEFAULT_SEARCH_COUNT = 20 + DEFAULT_SEARCH_PAGE = 1 +) + +type SearchParameters struct { + Sort string + SortDirection string + Highlight bool + Count int + Page int +} + +type CtxChannel struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type CtxMessage struct { + User string `json:"user"` + Username string `json:"username"` + Text string `json:"text"` + Timestamp string `json:"ts"` + Type string `json:"type"` +} + +type SearchMessage struct { + Type string `json:"type"` + Channel CtxChannel `json:"channel"` + User string `json:"user"` + Username string `json:"username"` + Timestamp string `json:"ts"` + Text string `json:"text"` + Permalink string `json:"permalink"` + Attachments []Attachment `json:"attachments"` + Previous CtxMessage `json:"previous"` + Previous2 CtxMessage `json:"previous_2"` + Next CtxMessage `json:"next"` + Next2 CtxMessage `json:"next_2"` +} + +type SearchMessages struct { + Matches []SearchMessage `json:"matches"` + Paging `json:"paging"` + Pagination `json:"pagination"` + Total int `json:"total"` +} + +type SearchFiles struct { + Matches []File `json:"matches"` + Paging `json:"paging"` + Pagination `json:"pagination"` + Total int `json:"total"` +} + +type searchResponseFull struct { + Query string `json:"query"` + SearchMessages `json:"messages"` + SearchFiles `json:"files"` + SlackResponse +} + +func NewSearchParameters() SearchParameters { + return SearchParameters{ + Sort: DEFAULT_SEARCH_SORT, + SortDirection: DEFAULT_SEARCH_SORT_DIR, + Highlight: DEFAULT_SEARCH_HIGHLIGHT, + Count: DEFAULT_SEARCH_COUNT, + Page: DEFAULT_SEARCH_PAGE, + } +} + +func (api *Client) _search(ctx context.Context, path, query string, params SearchParameters, files, messages bool) (response *searchResponseFull, error error) { + values := url.Values{ + "token": {api.token}, + "query": {query}, + } + if params.Sort != DEFAULT_SEARCH_SORT { + values.Add("sort", params.Sort) + } + if params.SortDirection != DEFAULT_SEARCH_SORT_DIR { + values.Add("sort_dir", params.SortDirection) + } + if params.Highlight != DEFAULT_SEARCH_HIGHLIGHT { + values.Add("highlight", strconv.Itoa(1)) + } + if params.Count != DEFAULT_SEARCH_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Page != DEFAULT_SEARCH_PAGE { + values.Add("page", strconv.Itoa(params.Page)) + } + + response = &searchResponseFull{} + err := postPath(ctx, api.httpclient, path, values, response, api.debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil + +} + +func (api *Client) Search(query string, params SearchParameters) (*SearchMessages, *SearchFiles, error) { + return api.SearchContext(context.Background(), query, params) +} + +func (api *Client) SearchContext(ctx context.Context, query string, params SearchParameters) (*SearchMessages, *SearchFiles, error) { + response, err := api._search(ctx, "search.all", query, params, true, true) + if err != nil { + return nil, nil, err + } + return &response.SearchMessages, &response.SearchFiles, nil +} + +func (api *Client) SearchFiles(query string, params SearchParameters) (*SearchFiles, error) { + return api.SearchFilesContext(context.Background(), query, params) +} + +func (api *Client) SearchFilesContext(ctx context.Context, query string, params SearchParameters) (*SearchFiles, error) { + response, err := api._search(ctx, "search.files", query, params, true, false) + if err != nil { + return nil, err + } + return &response.SearchFiles, nil +} + +func (api *Client) SearchMessages(query string, params SearchParameters) (*SearchMessages, error) { + return api.SearchMessagesContext(context.Background(), query, params) +} + +func (api *Client) SearchMessagesContext(ctx context.Context, query string, params SearchParameters) (*SearchMessages, error) { + response, err := api._search(ctx, "search.messages", query, params, false, true) + if err != nil { + return nil, err + } + return &response.SearchMessages, nil +} diff --git a/vendor/github.com/nlopes/slack/slack.go b/vendor/github.com/nlopes/slack/slack.go new file mode 100644 index 0000000..88c2d04 --- /dev/null +++ b/vendor/github.com/nlopes/slack/slack.go @@ -0,0 +1,140 @@ +package slack + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "net/url" + "os" +) + +// Added as a var so that we can change this for testing purposes +var SLACK_API string = "https://slack.com/api/" +var SLACK_WEB_API_FORMAT string = "https://%s.slack.com/api/users.admin.%s?t=%s" + +// 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 +} + +// ResponseMetadata holds pagination metadata +type ResponseMetadata struct { + Cursor string `json:"next_cursor"` +} + +func (t *ResponseMetadata) initialize() *ResponseMetadata { + if t != nil { + return t + } + + return &ResponseMetadata{} +} + +type AuthTestResponse struct { + URL string `json:"url"` + Team string `json:"team"` + User string `json:"user"` + TeamID string `json:"team_id"` + UserID string `json:"user_id"` +} + +type authTestResponseFull struct { + SlackResponse + AuthTestResponse +} + +type Client struct { + 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 + } +} + +// 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) + } + + return s +} + +// AuthTest tests if the user is able to do authenticated requests or not +func (api *Client) AuthTest() (response *AuthTestResponse, error error) { + return api.AuthTestContext(context.Background()) +} + +// 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 := postPath(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 +} + +// SetDebug switches the api into debug mode +// When in debug mode, it logs various info about what its doing +// If you ever use this in production, don't call SetDebug(true) +func (api *Client) SetDebug(debug bool) { + api.debug = debug + if debug && logger == nil { + SetLogger(log.New(os.Stdout, "nlopes/slack", log.LstdFlags|log.Lshortfile)) + } +} + +// Debugf print a formatted debug line. +func (api *Client) Debugf(format string, v ...interface{}) { + if api.debug { + logger.Output(2, fmt.Sprintf(format, v...)) + } +} + +// Debugln print a debug line. +func (api *Client) Debugln(v ...interface{}) { + if api.debug { + logger.Output(2, fmt.Sprintln(v...)) + } +} diff --git a/vendor/github.com/nlopes/slack/slash.go b/vendor/github.com/nlopes/slack/slash.go new file mode 100644 index 0000000..f62065a --- /dev/null +++ b/vendor/github.com/nlopes/slack/slash.go @@ -0,0 +1,53 @@ +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"` + EnterpriseID string `json:"enterprise_id,omitempty"` + EnterpriseName string `json:"enterprise_name,omitempty"` + 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.EnterpriseID = r.PostForm.Get("enterprise_id") + s.EnterpriseName = r.PostForm.Get("enterprise_name") + 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 new file mode 100644 index 0000000..d01d242 --- /dev/null +++ b/vendor/github.com/nlopes/slack/stars.go @@ -0,0 +1,159 @@ +package slack + +import ( + "context" + "errors" + "net/url" + "strconv" +) + +const ( + DEFAULT_STARS_USER = "" + DEFAULT_STARS_COUNT = 100 + DEFAULT_STARS_PAGE = 1 +) + +type StarsParameters struct { + User string + Count int + Page int +} + +type StarredItem Item + +type listResponseFull struct { + Items []Item `json:"items"` + Paging `json:"paging"` + SlackResponse +} + +// NewStarsParameters initialises StarsParameters with default values +func NewStarsParameters() StarsParameters { + return StarsParameters{ + User: DEFAULT_STARS_USER, + Count: DEFAULT_STARS_COUNT, + Page: DEFAULT_STARS_PAGE, + } +} + +// AddStar stars an item in a channel +func (api *Client) AddStar(channel string, item ItemRef) error { + return api.AddStarContext(context.Background(), channel, item) +} + +// AddStarContext stars an item in a channel with a custom context +func (api *Client) AddStarContext(ctx context.Context, channel string, item ItemRef) error { + values := url.Values{ + "channel": {channel}, + "token": {api.token}, + } + if item.Timestamp != "" { + values.Set("timestamp", item.Timestamp) + } + if item.File != "" { + values.Set("file", item.File) + } + if item.Comment != "" { + values.Set("file_comment", item.Comment) + } + + response := &SlackResponse{} + if err := postPath(ctx, api.httpclient, "stars.add", values, response, api.debug); err != nil { + return err + } + + return response.Err() +} + +// RemoveStar removes a starred item from a channel +func (api *Client) RemoveStar(channel string, item ItemRef) error { + return api.RemoveStarContext(context.Background(), channel, item) +} + +// RemoveStarContext removes a starred item from a channel with a custom context +func (api *Client) RemoveStarContext(ctx context.Context, channel string, item ItemRef) error { + values := url.Values{ + "channel": {channel}, + "token": {api.token}, + } + if item.Timestamp != "" { + values.Set("timestamp", item.Timestamp) + } + if item.File != "" { + values.Set("file", item.File) + } + if item.Comment != "" { + values.Set("file_comment", item.Comment) + } + + response := &SlackResponse{} + if err := postPath(ctx, api.httpclient, "stars.remove", values, response, api.debug); err != nil { + return err + } + + return response.Err() +} + +// ListStars returns information about the stars a user added +func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) { + return api.ListStarsContext(context.Background(), params) +} + +// 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.token}, + } + if params.User != DEFAULT_STARS_USER { + values.Add("user", params.User) + } + if params.Count != DEFAULT_STARS_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Page != DEFAULT_STARS_PAGE { + values.Add("page", strconv.Itoa(params.Page)) + } + + response := &listResponseFull{} + err := postPath(ctx, api.httpclient, "stars.list", values, response, api.debug) + if err != nil { + return nil, nil, err + } + if !response.Ok { + return nil, nil, errors.New(response.Error) + } + return response.Items, &response.Paging, nil +} + +// GetStarred returns a list of StarredItem items. +// +// The user then has to iterate over them and figure out what they should +// be looking at according to what is in the Type. +// for _, item := range items { +// switch c.Type { +// case "file_comment": +// log.Println(c.Comment) +// case "file": +// ... +// +// } +// This function still exists to maintain backwards compatibility. +// I exposed it as returning []StarredItem, so it shall stay as StarredItem +func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, *Paging, error) { + return api.GetStarredContext(context.Background(), params) +} + +// GetStarredContext returns a list of StarredItem items with a custom context +// +// For more details see GetStarred +func (api *Client) GetStarredContext(ctx context.Context, params StarsParameters) ([]StarredItem, *Paging, error) { + items, paging, err := api.ListStarsContext(ctx, params) + if err != nil { + return nil, nil, err + } + starredItems := make([]StarredItem, len(items)) + for i, item := range items { + starredItems[i] = StarredItem(item) + } + return starredItems, paging, nil +} diff --git a/vendor/github.com/nlopes/slack/team.go b/vendor/github.com/nlopes/slack/team.go new file mode 100644 index 0000000..cf19fb7 --- /dev/null +++ b/vendor/github.com/nlopes/slack/team.go @@ -0,0 +1,177 @@ +package slack + +import ( + "context" + "errors" + "net/url" + "strconv" +) + +const ( + DEFAULT_LOGINS_COUNT = 100 + DEFAULT_LOGINS_PAGE = 1 +) + +type TeamResponse struct { + Team TeamInfo `json:"team"` + SlackResponse +} + +type TeamInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` + EmailDomain string `json:"email_domain"` + Icon map[string]interface{} `json:"icon"` +} + +type LoginResponse struct { + Logins []Login `json:"logins"` + Paging `json:"paging"` + SlackResponse +} + +type Login struct { + UserID string `json:"user_id"` + Username string `json:"username"` + DateFirst int `json:"date_first"` + DateLast int `json:"date_last"` + Count int `json:"count"` + IP string `json:"ip"` + UserAgent string `json:"user_agent"` + ISP string `json:"isp"` + Country string `json:"country"` + Region string `json:"region"` +} + +type BillableInfoResponse struct { + BillableInfo map[string]BillingActive `json:"billable_info"` + SlackResponse +} + +type BillingActive struct { + BillingActive bool `json:"billing_active"` +} + +// AccessLogParameters contains all the parameters necessary (including the optional ones) for a GetAccessLogs() request +type AccessLogParameters struct { + Count int + Page int +} + +// NewAccessLogParameters provides an instance of AccessLogParameters with all the sane default values set +func NewAccessLogParameters() AccessLogParameters { + return AccessLogParameters{ + Count: DEFAULT_LOGINS_COUNT, + Page: DEFAULT_LOGINS_PAGE, + } +} + +func teamRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*TeamResponse, error) { + response := &TeamResponse{} + err := postPath(ctx, client, path, values, response, debug) + if err != nil { + return nil, err + } + + if !response.Ok { + return nil, errors.New(response.Error) + } + + return response, nil +} + +func billableInfoRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (map[string]BillingActive, error) { + response := &BillableInfoResponse{} + err := postPath(ctx, client, path, values, response, debug) + if err != nil { + return nil, err + } + + if !response.Ok { + return nil, errors.New(response.Error) + } + + return response.BillableInfo, nil +} + +func accessLogsRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*LoginResponse, error) { + response := &LoginResponse{} + err := postPath(ctx, client, path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// GetTeamInfo gets the Team Information of the user +func (api *Client) GetTeamInfo() (*TeamInfo, error) { + return api.GetTeamInfoContext(context.Background()) +} + +// 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.token}, + } + + response, err := teamRequest(ctx, api.httpclient, "team.info", values, api.debug) + if err != nil { + return nil, err + } + return &response.Team, nil +} + +// GetAccessLogs retrieves a page of logins according to the parameters given +func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging, error) { + return api.GetAccessLogsContext(context.Background(), params) +} + +// 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.token}, + } + if params.Count != DEFAULT_LOGINS_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Page != DEFAULT_LOGINS_PAGE { + values.Add("page", strconv.Itoa(params.Page)) + } + + response, err := accessLogsRequest(ctx, api.httpclient, "team.accessLogs", values, api.debug) + if err != nil { + return nil, nil, err + } + return response.Logins, &response.Paging, nil +} + +func (api *Client) GetBillableInfo(user string) (map[string]BillingActive, error) { + return api.GetBillableInfoContext(context.Background(), user) +} + +func (api *Client) GetBillableInfoContext(ctx context.Context, user string) (map[string]BillingActive, error) { + values := url.Values{ + "token": {api.token}, + "user": {user}, + } + + return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug) +} + +// GetBillableInfoForTeam returns the billing_active status of all users on the team. +func (api *Client) GetBillableInfoForTeam() (map[string]BillingActive, error) { + return api.GetBillableInfoForTeamContext(context.Background()) +} + +// 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.token}, + } + + 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 new file mode 100644 index 0000000..87d1cda --- /dev/null +++ b/vendor/github.com/nlopes/slack/usergroups.go @@ -0,0 +1,210 @@ +package slack + +import ( + "context" + "errors" + "net/url" + "strings" +) + +// UserGroup contains all the information of a user group +type UserGroup struct { + ID string `json:"id"` + TeamID string `json:"team_id"` + IsUserGroup bool `json:"is_usergroup"` + Name string `json:"name"` + Description string `json:"description"` + Handle string `json:"handle"` + IsExternal bool `json:"is_external"` + DateCreate JSONTime `json:"date_create"` + DateUpdate JSONTime `json:"date_update"` + DateDelete JSONTime `json:"date_delete"` + AutoType string `json:"auto_type"` + CreatedBy string `json:"created_by"` + UpdatedBy string `json:"updated_by"` + DeletedBy string `json:"deleted_by"` + Prefs UserGroupPrefs `json:"prefs"` + UserCount int `json:"user_count"` +} + +// UserGroupPrefs contains default channels and groups (private channels) +type UserGroupPrefs struct { + Channels []string `json:"channels"` + Groups []string `json:"groups"` +} + +type userGroupResponseFull struct { + UserGroups []UserGroup `json:"usergroups"` + UserGroup UserGroup `json:"usergroup"` + Users []string `json:"users"` + SlackResponse +} + +func userGroupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userGroupResponseFull, error) { + response := &userGroupResponseFull{} + err := postPath(ctx, client, path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// CreateUserGroup creates a new user group +func (api *Client) CreateUserGroup(userGroup UserGroup) (UserGroup, error) { + return api.CreateUserGroupContext(context.Background(), userGroup) +} + +// 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.token}, + "name": {userGroup.Name}, + } + + if userGroup.Handle != "" { + values["handle"] = []string{userGroup.Handle} + } + + if userGroup.Description != "" { + values["description"] = []string{userGroup.Description} + } + + if len(userGroup.Prefs.Channels) > 0 { + values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")} + } + + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.create", values, api.debug) + if err != nil { + return UserGroup{}, err + } + return response.UserGroup, nil +} + +// DisableUserGroup disables an existing user group +func (api *Client) DisableUserGroup(userGroup string) (UserGroup, error) { + return api.DisableUserGroupContext(context.Background(), userGroup) +} + +// 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.token}, + "usergroup": {userGroup}, + } + + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.disable", values, api.debug) + if err != nil { + return UserGroup{}, err + } + return response.UserGroup, nil +} + +// EnableUserGroup enables an existing user group +func (api *Client) EnableUserGroup(userGroup string) (UserGroup, error) { + return api.EnableUserGroupContext(context.Background(), userGroup) +} + +// 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.token}, + "usergroup": {userGroup}, + } + + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.enable", values, api.debug) + if err != nil { + return UserGroup{}, err + } + return response.UserGroup, nil +} + +// GetUserGroups returns a list of user groups for the team +func (api *Client) GetUserGroups() ([]UserGroup, error) { + return api.GetUserGroupsContext(context.Background()) +} + +// 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.token}, + } + + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.list", values, api.debug) + if err != nil { + return nil, err + } + return response.UserGroups, nil +} + +// UpdateUserGroup will update an existing user group +func (api *Client) UpdateUserGroup(userGroup UserGroup) (UserGroup, error) { + return api.UpdateUserGroupContext(context.Background(), userGroup) +} + +// 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.token}, + "usergroup": {userGroup.ID}, + } + + if userGroup.Name != "" { + values["name"] = []string{userGroup.Name} + } + + if userGroup.Handle != "" { + values["handle"] = []string{userGroup.Handle} + } + + if userGroup.Description != "" { + values["description"] = []string{userGroup.Description} + } + + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.update", values, api.debug) + if err != nil { + return UserGroup{}, err + } + return response.UserGroup, nil +} + +// GetUserGroupMembers will retrieve the current list of users in a group +func (api *Client) GetUserGroupMembers(userGroup string) ([]string, error) { + return api.GetUserGroupMembersContext(context.Background(), userGroup) +} + +// 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.token}, + "usergroup": {userGroup}, + } + + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.list", values, api.debug) + if err != nil { + return []string{}, err + } + return response.Users, nil +} + +// UpdateUserGroupMembers will update the members of an existing user group +func (api *Client) UpdateUserGroupMembers(userGroup string, members string) (UserGroup, error) { + return api.UpdateUserGroupMembersContext(context.Background(), userGroup, members) +} + +// 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.token}, + "usergroup": {userGroup}, + "users": {members}, + } + + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.update", values, api.debug) + if err != nil { + return UserGroup{}, err + } + return response.UserGroup, nil +} diff --git a/vendor/github.com/nlopes/slack/users.go b/vendor/github.com/nlopes/slack/users.go new file mode 100644 index 0000000..1601150 --- /dev/null +++ b/vendor/github.com/nlopes/slack/users.go @@ -0,0 +1,558 @@ +package slack + +import ( + "context" + "encoding/json" + "errors" + "net/url" + "strconv" +) + +const ( + DEFAULT_USER_PHOTO_CROP_X = -1 + DEFAULT_USER_PHOTO_CROP_Y = -1 + DEFAULT_USER_PHOTO_CROP_W = -1 + errPaginationComplete = errorString("pagination complete") +) + +// UserProfile contains all the information details of a given user +type UserProfile struct { + 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"` + Fields UserProfileCustomFields `json:"fields"` +} + +// UserProfileCustomFields represents user profile's custom fields. +// Slack API's response data type is inconsistent so we use the struct. +// For detail, please see below. +// https://github.com/nlopes/slack/pull/298#discussion_r185159233 +type UserProfileCustomFields struct { + fields map[string]UserProfileCustomField +} + +// UnmarshalJSON is the implementation of the json.Unmarshaler interface. +func (fields *UserProfileCustomFields) UnmarshalJSON(b []byte) error { + // https://github.com/nlopes/slack/pull/298#discussion_r185159233 + if string(b) == "[]" { + return nil + } + return json.Unmarshal(b, &fields.fields) +} + +// MarshalJSON is the implementation of the json.Marshaler interface. +func (fields UserProfileCustomFields) MarshalJSON() ([]byte, error) { + if len(fields.fields) == 0 { + return []byte("[]"), nil + } + return json.Marshal(fields.fields) +} + +// ToMap returns a map of custom fields. +func (fields *UserProfileCustomFields) ToMap() map[string]UserProfileCustomField { + return fields.fields +} + +// Len returns the number of custom fields. +func (fields *UserProfileCustomFields) Len() int { + return len(fields.fields) +} + +// SetMap sets a map of custom fields. +func (fields *UserProfileCustomFields) SetMap(m map[string]UserProfileCustomField) { + fields.fields = m +} + +// FieldsMap returns a map of custom fields. +func (profile *UserProfile) FieldsMap() map[string]UserProfileCustomField { + return profile.Fields.ToMap() +} + +// SetFieldsMap sets a map of custom fields. +func (profile *UserProfile) SetFieldsMap(m map[string]UserProfileCustomField) { + profile.Fields.SetMap(m) +} + +// UserProfileCustomField represents a custom user profile field +type UserProfileCustomField struct { + Value string `json:"value"` + Alt string `json:"alt"` + Label string `json:"label"` +} + +// User contains all the information of a user +type User struct { + ID string `json:"id"` + TeamID string `json:"team_id"` + Name string `json:"name"` + Deleted bool `json:"deleted"` + Color string `json:"color"` + RealName string `json:"real_name"` + TZ string `json:"tz,omitempty"` + TZLabel string `json:"tz_label"` + TZOffset int `json:"tz_offset"` + Profile UserProfile `json:"profile"` + IsBot bool `json:"is_bot"` + IsAdmin bool `json:"is_admin"` + IsOwner bool `json:"is_owner"` + IsPrimaryOwner bool `json:"is_primary_owner"` + IsRestricted bool `json:"is_restricted"` + IsUltraRestricted bool `json:"is_ultra_restricted"` + IsStranger bool `json:"is_stranger"` + IsAppUser bool `json:"is_app_user"` + Has2FA bool `json:"has_2fa"` + HasFiles bool `json:"has_files"` + Presence string `json:"presence"` + Locale string `json:"locale"` +} + +// UserPresence contains details about a user online status +type UserPresence struct { + Presence string `json:"presence,omitempty"` + Online bool `json:"online,omitempty"` + AutoAway bool `json:"auto_away,omitempty"` + ManualAway bool `json:"manual_away,omitempty"` + ConnectionCount int `json:"connection_count,omitempty"` + LastActivity JSONTime `json:"last_activity,omitempty"` +} + +type UserIdentityResponse struct { + User UserIdentity `json:"user"` + Team TeamIdentity `json:"team"` + SlackResponse +} + +type UserIdentity struct { + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + 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"` + Image512 string `json:"image_512"` +} + +type TeamIdentity struct { + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` + Image34 string `json:"image_34"` + Image44 string `json:"image_44"` + Image68 string `json:"image_68"` + Image88 string `json:"image_88"` + Image102 string `json:"image_102"` + Image132 string `json:"image_132"` + Image230 string `json:"image_230"` + ImageDefault bool `json:"image_default"` + ImageOriginal string `json:"image_original"` +} + +type userResponseFull struct { + Members []User `json:"members,omitempty"` + User `json:"user,omitempty"` + UserPresence + SlackResponse + Metadata ResponseMetadata `json:"response_metadata"` +} + +type UserSetPhotoParams struct { + CropX int + CropY int + CropW int +} + +func NewUserSetPhotoParams() UserSetPhotoParams { + return UserSetPhotoParams{ + CropX: DEFAULT_USER_PHOTO_CROP_X, + CropY: DEFAULT_USER_PHOTO_CROP_Y, + CropW: DEFAULT_USER_PHOTO_CROP_W, + } +} + +func userRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userResponseFull, error) { + response := &userResponseFull{} + err := postForm(ctx, client, SLACK_API+path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// GetUserPresence will retrieve the current presence status of given user. +func (api *Client) GetUserPresence(user string) (*UserPresence, error) { + return api.GetUserPresenceContext(context.Background(), user) +} + +// 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.token}, + "user": {user}, + } + + response, err := userRequest(ctx, api.httpclient, "users.getPresence", values, api.debug) + if err != nil { + return nil, err + } + return &response.UserPresence, nil +} + +// GetUserInfo will retrieve the complete user information +func (api *Client) GetUserInfo(user string) (*User, error) { + return api.GetUserInfoContext(context.Background(), user) +} + +// GetUserInfoContext will retrieve the complete user information with a custom context +func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, error) { + values := url.Values{ + "token": {api.token}, + "user": {user}, + } + + response, err := userRequest(ctx, api.httpclient, "users.info", values, api.debug) + if err != nil { + return nil, err + } + return &response.User, nil +} + +// GetUsersOption options for the GetUsers method call. +type GetUsersOption func(*UserPagination) + +// GetUsersOptionLimit limit the number of users returned +func GetUsersOptionLimit(n int) GetUsersOption { + return func(p *UserPagination) { + p.limit = n + } +} + +// GetUsersOptionPresence include user presence +func GetUsersOptionPresence(n bool) GetUsersOption { + return func(p *UserPagination) { + p.presence = n + } +} + +func newUserPagination(c *Client, options ...GetUsersOption) (up UserPagination) { + up = UserPagination{ + c: c, + limit: 200, // per slack api documentation. + } + + for _, opt := range options { + opt(&up) + } + + return up +} + +// UserPagination allows for paginating over the users +type UserPagination struct { + Users []User + limit int + presence bool + previousResp *ResponseMetadata + c *Client +} + +// Done checks if the pagination has completed +func (UserPagination) Done(err error) bool { + return err == errPaginationComplete +} + +// Failure checks if pagination failed. +func (t UserPagination) Failure(err error) error { + if t.Done(err) { + return nil + } + + return err +} + +func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error) { + var ( + resp *userResponseFull + ) + + if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") { + return t, errPaginationComplete + } + + t.previousResp = t.previousResp.initialize() + + values := url.Values{ + "limit": {strconv.Itoa(t.limit)}, + "presence": {strconv.FormatBool(t.presence)}, + "token": {t.c.token}, + "cursor": {t.previousResp.Cursor}, + } + + if resp, err = userRequest(ctx, t.c.httpclient, "users.list", values, t.c.debug); err != nil { + return t, err + } + + t.c.Debugf("GetUsersContext: got %d users; metadata %v", len(resp.Members), resp.Metadata) + t.Users = resp.Members + t.previousResp = &resp.Metadata + + return t, nil +} + +// GetUsersPaginated fetches users in a paginated fashion, see GetUsersContext for usage. +func (api *Client) GetUsersPaginated(options ...GetUsersOption) UserPagination { + return newUserPagination(api, options...) +} + +// GetUsers returns the list of users (with their detailed information) +func (api *Client) GetUsers() ([]User, error) { + return api.GetUsersContext(context.Background()) +} + +// GetUsersContext returns the list of users (with their detailed information) with a custom context +func (api *Client) GetUsersContext(ctx context.Context) (results []User, err error) { + var ( + p UserPagination + ) + + for p = api.GetUsersPaginated(); !p.Done(err); p, err = p.Next(ctx) { + results = append(results, p.Users...) + } + + return results, p.Failure(err) +} + +// 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) (err error) { + values := url.Values{ + "token": {api.token}, + } + + _, err = userRequest(ctx, api.httpclient, "users.setActive", values, api.debug) + return err +} + +// SetUserPresence changes the currently authenticated user presence +func (api *Client) SetUserPresence(presence string) error { + return api.SetUserPresenceContext(context.Background(), presence) +} + +// 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.token}, + "presence": {presence}, + } + + _, err := userRequest(ctx, api.httpclient, "users.setPresence", values, api.debug) + return err +} + +// GetUserIdentity will retrieve user info available per identity scopes +func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) { + return api.GetUserIdentityContext(context.Background()) +} + +// 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.token}, + } + response := &UserIdentityResponse{} + + err := postForm(ctx, api.httpclient, SLACK_API+"users.identity", values, response, api.debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// SetUserPhoto changes the currently authenticated user's profile image +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.token}, + } + if params.CropX != DEFAULT_USER_PHOTO_CROP_X { + values.Add("crop_x", strconv.Itoa(params.CropX)) + } + if params.CropY != DEFAULT_USER_PHOTO_CROP_Y { + values.Add("crop_y", strconv.Itoa(params.CropX)) + } + if params.CropW != DEFAULT_USER_PHOTO_CROP_W { + values.Add("crop_w", strconv.Itoa(params.CropW)) + } + + err := postLocalWithMultipartResponse(ctx, api.httpclient, "users.setPhoto", image, "image", values, response, api.debug) + if err != nil { + return err + } + + return response.Err() +} + +// DeleteUserPhoto deletes the current authenticated user's profile image +func (api *Client) DeleteUserPhoto() error { + return api.DeleteUserPhotoContext(context.Background()) +} + +// DeleteUserPhotoContext deletes the current authenticated user's profile image with a custom context +func (api *Client) DeleteUserPhotoContext(ctx context.Context) error { + response := &SlackResponse{} + values := url.Values{ + "token": {api.token}, + } + + err := postForm(ctx, api.httpclient, SLACK_API+"users.deletePhoto", values, response, api.debug) + if err != nil { + return err + } + + return response.Err() +} + +// SetUserCustomStatus will set a custom status and emoji for the currently +// authenticated user. If statusEmoji is "" and statusText is not, the Slack API +// will automatically set it to ":speech_balloon:". Otherwise, if both are "" +// the Slack API will unset the custom status/emoji. +func (api *Client) SetUserCustomStatus(statusText, statusEmoji string) error { + return api.SetUserCustomStatusContext(context.Background(), statusText, statusEmoji) +} + +// SetUserCustomStatusContext will set a custom status and emoji for the currently authenticated user with a custom context +// +// For more information see SetUserCustomStatus +func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, statusEmoji string) error { + // XXX(theckman): this anonymous struct is for making requests to the Slack + // API for setting and unsetting a User's Custom Status/Emoji. To change + // these values we must provide a JSON document as the profile POST field. + // + // We use an anonymous struct over UserProfile because to unset the values + // on the User's profile we cannot use the `json:"omitempty"` tag. This is + // because an empty string ("") is what's used to unset the values. Check + // out the API docs for more details: + // + // - https://api.slack.com/docs/presence-and-status#custom_status + profile, err := json.Marshal( + &struct { + StatusText string `json:"status_text"` + StatusEmoji string `json:"status_emoji"` + }{ + StatusText: statusText, + StatusEmoji: statusEmoji, + }, + ) + + if err != nil { + return err + } + + values := url.Values{ + "token": {api.token}, + "profile": {string(profile)}, + } + + response := &userResponseFull{} + if err = postForm(ctx, api.httpclient, SLACK_API+"users.profile.set", values, response, api.debug); err != nil { + return err + } + + if !response.Ok { + return errors.New(response.Error) + } + + return nil +} + +// UnsetUserCustomStatus removes the custom status message for the currently +// authenticated user. This is a convenience method that wraps (*Client).SetUserCustomStatus(). +func (api *Client) UnsetUserCustomStatus() error { + return api.UnsetUserCustomStatusContext(context.Background()) +} + +// UnsetUserCustomStatusContext removes the custom status message for the currently authenticated user +// with a custom context. This is a convenience method that wraps (*Client).SetUserCustomStatus(). +func (api *Client) UnsetUserCustomStatusContext(ctx context.Context) error { + return api.SetUserCustomStatusContext(ctx, "", "") +} + +// GetUserProfile retrieves a user's profile information. +func (api *Client) GetUserProfile(userID string, includeLabels bool) (*UserProfile, error) { + return api.GetUserProfileContext(context.Background(), userID, includeLabels) +} + +type getUserProfileResponse struct { + SlackResponse + Profile *UserProfile `json:"profile"` +} + +// GetUserProfileContext retrieves a user's profile information with a context. +func (api *Client) GetUserProfileContext(ctx context.Context, userID string, includeLabels bool) (*UserProfile, error) { + values := url.Values{"token": {api.token}, "user": {userID}} + if includeLabels { + values.Add("include_labels", "true") + } + resp := &getUserProfileResponse{} + + err := postPath(ctx, api.httpclient, "users.profile.get", values, &resp, api.debug) + if err != nil { + return nil, err + } + if !resp.Ok { + return nil, errors.New(resp.Error) + } + return resp.Profile, nil +} diff --git a/vendor/github.com/nlopes/slack/websocket.go b/vendor/github.com/nlopes/slack/websocket.go new file mode 100644 index 0000000..c5780cc --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket.go @@ -0,0 +1,113 @@ +package slack + +import ( + "encoding/json" + "errors" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +const ( + // MaxMessageTextLength is the current maximum message length in number of characters as defined here + // https://api.slack.com/rtm#limits + MaxMessageTextLength = 4000 +) + +// RTM represents a managed websocket connection. It also supports +// all the methods of the `Client` type. +// +// Create this element with Client's NewRTM() or NewRTMWithOptions(*RTMOptions) +type RTM struct { + idGen IDGenerator + pingInterval time.Duration + pingDeadman *time.Timer + + // Connection life-cycle + conn *websocket.Conn + 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 + isConnected bool + + // Client is the main API, embedded + Client + websocketURL string + + // UserDetails upon connection + info *Info + + // useRTMStart should be set to true if you want to use + // rtm.start to connect to Slack, otherwise it will use + // rtm.connect + useRTMStart bool + + // dialer is a gorilla/websocket Dialer. If nil, use the default + // Dialer. + dialer *websocket.Dialer + + // mu is mutex used to prevent RTM connection race conditions + mu *sync.Mutex +} + +// RTMOptions allows configuration of various options available for RTM messaging +// +// This structure will evolve in time so please make sure you are always using the +// named keys for every entry available as per Go 1 compatibility promise adding fields +// to this structure should not be considered a breaking change. +type RTMOptions struct { + // UseRTMStart set to true in order to use rtm.start or false to use rtm.connect + // As of 11th July 2017 you should prefer setting this to false, see: + // https://api.slack.com/changelog/2017-04-start-using-rtm-connect-and-stop-using-rtm-start + UseRTMStart bool +} + +// Disconnect and wait, blocking until a successful disconnection. +func (rtm *RTM) Disconnect() error { + // avoid RTM disconnect race conditions + rtm.mu.Lock() + defer rtm.mu.Unlock() + // 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 +} + +// GetInfo returns the info structure received when calling +// "startrtm", holding all channels, groups and other metadata needed +// to implement a full chat client. It will be non-nil after a call to +// StartRTM(). +func (rtm *RTM) GetInfo() *Info { + return rtm.info +} + +// SendMessage submits a simple message through the websocket. For +// more complicated messages, use `rtm.PostMessage` with a complete +// struct describing your attachments and all. +func (rtm *RTM) SendMessage(msg *OutgoingMessage) { + if msg == nil { + rtm.Debugln("Error: Attempted to SendMessage(nil)") + return + } + + rtm.outgoingMessages <- *msg +} + +func (rtm *RTM) resetDeadman() { + timerReset(rtm.pingDeadman, deadmanDuration(rtm.pingInterval)) +} + +func deadmanDuration(d time.Duration) time.Duration { + return d * 4 +} diff --git a/vendor/github.com/nlopes/slack/websocket_channels.go b/vendor/github.com/nlopes/slack/websocket_channels.go new file mode 100644 index 0000000..7dd3319 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_channels.go @@ -0,0 +1,72 @@ +package slack + +// ChannelCreatedEvent represents the Channel created event +type ChannelCreatedEvent struct { + Type string `json:"type"` + Channel ChannelCreatedInfo `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelCreatedInfo represents the information associated with the Channel created event +type ChannelCreatedInfo struct { + ID string `json:"id"` + IsChannel bool `json:"is_channel"` + Name string `json:"name"` + Created int `json:"created"` + Creator string `json:"creator"` +} + +// ChannelJoinedEvent represents the Channel joined event +type ChannelJoinedEvent struct { + Type string `json:"type"` + Channel Channel `json:"channel"` +} + +// ChannelInfoEvent represents the Channel info event +type ChannelInfoEvent struct { + // channel_left + // channel_deleted + // channel_archive + // channel_unarchive + Type string `json:"type"` + Channel string `json:"channel"` + User string `json:"user,omitempty"` + Timestamp string `json:"ts,omitempty"` +} + +// ChannelRenameEvent represents the Channel rename event +type ChannelRenameEvent struct { + Type string `json:"type"` + Channel ChannelRenameInfo `json:"channel"` + Timestamp string `json:"event_ts"` +} + +// ChannelRenameInfo represents the information associated with a Channel rename event +type ChannelRenameInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Created string `json:"created"` +} + +// ChannelHistoryChangedEvent represents the Channel history changed event +type ChannelHistoryChangedEvent struct { + Type string `json:"type"` + Latest string `json:"latest"` + Timestamp string `json:"ts"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelMarkedEvent represents the Channel marked event +type ChannelMarkedEvent ChannelInfoEvent + +// ChannelLeftEvent represents the Channel left event +type ChannelLeftEvent ChannelInfoEvent + +// ChannelDeletedEvent represents the Channel deleted event +type ChannelDeletedEvent ChannelInfoEvent + +// ChannelArchiveEvent represents the Channel archive event +type ChannelArchiveEvent ChannelInfoEvent + +// ChannelUnarchiveEvent represents the Channel unarchive event +type ChannelUnarchiveEvent ChannelInfoEvent diff --git a/vendor/github.com/nlopes/slack/websocket_dm.go b/vendor/github.com/nlopes/slack/websocket_dm.go new file mode 100644 index 0000000..98bf6f8 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_dm.go @@ -0,0 +1,23 @@ +package slack + +// IMCreatedEvent represents the IM created event +type IMCreatedEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel ChannelCreatedInfo `json:"channel"` +} + +// IMHistoryChangedEvent represents the IM history changed event +type IMHistoryChangedEvent ChannelHistoryChangedEvent + +// IMOpenEvent represents the IM open event +type IMOpenEvent ChannelInfoEvent + +// IMCloseEvent represents the IM close event +type IMCloseEvent ChannelInfoEvent + +// IMMarkedEvent represents the IM marked event +type IMMarkedEvent ChannelInfoEvent + +// IMMarkedHistoryChanged represents the IM marked history changed event +type IMMarkedHistoryChanged ChannelInfoEvent diff --git a/vendor/github.com/nlopes/slack/websocket_dnd.go b/vendor/github.com/nlopes/slack/websocket_dnd.go new file mode 100644 index 0000000..62ddea3 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_dnd.go @@ -0,0 +1,8 @@ +package slack + +// DNDUpdatedEvent represents the update event for Do Not Disturb +type DNDUpdatedEvent struct { + Type string `json:"type"` + User string `json:"user"` + Status DNDStatus `json:"dnd_status"` +} diff --git a/vendor/github.com/nlopes/slack/websocket_files.go b/vendor/github.com/nlopes/slack/websocket_files.go new file mode 100644 index 0000000..8c5bd4f --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_files.go @@ -0,0 +1,49 @@ +package slack + +// FileActionEvent represents the File action event +type fileActionEvent struct { + Type string `json:"type"` + EventTimestamp string `json:"event_ts"` + File File `json:"file"` + // FileID is used for FileDeletedEvent + FileID string `json:"file_id,omitempty"` +} + +// FileCreatedEvent represents the File created event +type FileCreatedEvent fileActionEvent + +// FileSharedEvent represents the File shared event +type FileSharedEvent fileActionEvent + +// FilePublicEvent represents the File public event +type FilePublicEvent fileActionEvent + +// FileUnsharedEvent represents the File unshared event +type FileUnsharedEvent fileActionEvent + +// FileChangeEvent represents the File change event +type FileChangeEvent fileActionEvent + +// FileDeletedEvent represents the File deleted event +type FileDeletedEvent fileActionEvent + +// FilePrivateEvent represents the File private event +type FilePrivateEvent fileActionEvent + +// FileCommentAddedEvent represents the File comment added event +type FileCommentAddedEvent struct { + fileActionEvent + Comment Comment `json:"comment"` +} + +// FileCommentEditedEvent represents the File comment edited event +type FileCommentEditedEvent struct { + fileActionEvent + Comment Comment `json:"comment"` +} + +// FileCommentDeletedEvent represents the File comment deleted event +type FileCommentDeletedEvent struct { + fileActionEvent + Comment string `json:"comment"` +} diff --git a/vendor/github.com/nlopes/slack/websocket_groups.go b/vendor/github.com/nlopes/slack/websocket_groups.go new file mode 100644 index 0000000..eb88985 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_groups.go @@ -0,0 +1,49 @@ +package slack + +// GroupCreatedEvent represents the Group created event +type GroupCreatedEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel ChannelCreatedInfo `json:"channel"` +} + +// XXX: Should we really do this? event.Group is probably nicer than event.Channel +// even though the api returns "channel" + +// GroupMarkedEvent represents the Group marked event +type GroupMarkedEvent ChannelInfoEvent + +// GroupOpenEvent represents the Group open event +type GroupOpenEvent ChannelInfoEvent + +// GroupCloseEvent represents the Group close event +type GroupCloseEvent ChannelInfoEvent + +// GroupArchiveEvent represents the Group archive event +type GroupArchiveEvent ChannelInfoEvent + +// GroupUnarchiveEvent represents the Group unarchive event +type GroupUnarchiveEvent ChannelInfoEvent + +// GroupLeftEvent represents the Group left event +type GroupLeftEvent ChannelInfoEvent + +// GroupJoinedEvent represents the Group joined event +type GroupJoinedEvent ChannelJoinedEvent + +// GroupRenameEvent represents the Group rename event +type GroupRenameEvent struct { + Type string `json:"type"` + Group GroupRenameInfo `json:"channel"` + Timestamp string `json:"ts"` +} + +// GroupRenameInfo represents the group info related to the renamed group +type GroupRenameInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Created string `json:"created"` +} + +// GroupHistoryChangedEvent represents the Group history changed event +type GroupHistoryChangedEvent ChannelHistoryChangedEvent diff --git a/vendor/github.com/nlopes/slack/websocket_internals.go b/vendor/github.com/nlopes/slack/websocket_internals.go new file mode 100644 index 0000000..e8374b0 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_internals.go @@ -0,0 +1,99 @@ +package slack + +import ( + "fmt" + "time" +) + +/** + * Internal events, created by this lib and not mapped to Slack APIs. + */ + +// ConnectedEvent is used for when we connect to Slack +type ConnectedEvent struct { + ConnectionCount int // 1 = first time, 2 = second time + Info *Info +} + +// ConnectionErrorEvent contains information about a connection error +type ConnectionErrorEvent struct { + Attempt int + ErrorObj error +} + +func (c *ConnectionErrorEvent) Error() string { + return c.ErrorObj.Error() +} + +// ConnectingEvent contains information about our connection attempt +type ConnectingEvent struct { + Attempt int // 1 = first attempt, 2 = second attempt + ConnectionCount int +} + +// DisconnectedEvent contains information about how we disconnected +type DisconnectedEvent struct { + Intentional bool +} + +// LatencyReport contains information about connection latency +type LatencyReport struct { + Value time.Duration +} + +// InvalidAuthEvent is used in case we can't even authenticate with the API +type InvalidAuthEvent struct{} + +// UnmarshallingErrorEvent is used when there are issues deconstructing a response +type UnmarshallingErrorEvent struct { + ErrorObj error +} + +func (u UnmarshallingErrorEvent) Error() string { + return u.ErrorObj.Error() +} + +// MessageTooLongEvent is used when sending a message that is too long +type MessageTooLongEvent struct { + Message OutgoingMessage + MaxLength int +} + +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 + ErrorObj error +} + +func (o OutgoingErrorEvent) Error() string { + return o.ErrorObj.Error() +} + +// IncomingEventError contains information about an unexpected error receiving a websocket event +type IncomingEventError struct { + ErrorObj error +} + +func (i *IncomingEventError) Error() string { + return i.ErrorObj.Error() +} + +// AckErrorEvent i +type AckErrorEvent struct { + ErrorObj error +} + +func (a *AckErrorEvent) Error() string { + return a.ErrorObj.Error() +} diff --git a/vendor/github.com/nlopes/slack/websocket_managed_conn.go b/vendor/github.com/nlopes/slack/websocket_managed_conn.go new file mode 100644 index 0000000..272f4b6 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_managed_conn.go @@ -0,0 +1,530 @@ +package slack + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "time" + + "github.com/gorilla/websocket" +) + +// ManageConnection can be called on a Slack RTM instance returned by the +// NewRTM method. It will connect to the slack RTM API and handle all incoming +// and outgoing events. If a connection fails then it will attempt to reconnect +// and will notify any listeners through an error event on the IncomingEvents +// channel. +// +// If the connection ends and the disconnect was unintentional then this will +// attempt to reconnect. +// +// This should only be called once per slack API! Otherwise expect undefined +// behavior. +// +// The defined error events are located in websocket_internals.go. +func (rtm *RTM) ManageConnection() { + var ( + err error + connectionCount int + info *Info + conn *websocket.Conn + ) + + for { + // BEGIN SENSITIVE CODE, make sure lock is unlocked in this section. + rtm.mu.Lock() + connectionCount++ + // start trying to connect + // the returned err is already passed onto the IncomingEvents channel + if info, conn, err = rtm.connect(connectionCount, rtm.useRTMStart); err != nil { + // when the connection is unsuccessful its fatal, and we need to bail out. + rtm.Debugf("Failed to connect with RTM on try %d: %s", connectionCount, err) + rtm.mu.Unlock() + return + } + + rtm.info = info + rtm.IncomingEvents <- RTMEvent{"connected", &ConnectedEvent{ + ConnectionCount: connectionCount, + Info: info, + }} + + rtm.conn = conn + rtm.isConnected = true + rtm.mu.Unlock() + // END SENSITIVE CODE + + rtm.Debugf("RTM connection succeeded on try %d", connectionCount) + + keepRunning := make(chan bool) + // we're now connected (or have failed fatally) so we can set up + // listeners + go rtm.handleIncomingEvents(keepRunning) + + // this should be a blocking call until the connection has ended + rtm.handleEvents(keepRunning) + + // after being disconnected we need to check if it was intentional + // if not then we should try to reconnect + if rtm.wasIntentional { + return + } + // else continue and run the loop again to connect + } +} + +// connect attempts to connect to the slack websocket API. It handles any +// errors that occur while connecting and will return once a connection +// has been successfully opened. +// If useRTMStart is false then it uses rtm.connect to create the connection, +// otherwise it uses rtm.start. +func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocket.Conn, error) { + const ( + errInvalidAuth = "invalid_auth" + errInactiveAccount = "account_inactive" + errMissingAuthToken = "not_authed" + ) + + // used to provide exponential backoff wait time with jitter before trying + // to connect to slack again + boff := &backoff{ + Min: 100 * time.Millisecond, + Max: 5 * time.Minute, + Factor: 2, + Jitter: true, + } + + for { + // send connecting event + rtm.IncomingEvents <- RTMEvent{"connecting", &ConnectingEvent{ + Attempt: boff.attempts + 1, + ConnectionCount: connectionCount, + }} + // attempt to start the connection + info, conn, err := rtm.startRTMAndDial(useRTMStart) + if err == nil { + return info, conn, nil + } + + // check for fatal errors + switch err.Error() { + case errInvalidAuth, errInactiveAccount, errMissingAuthToken: + rtm.Debugf("Invalid auth when connecting with RTM: %s", err) + rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}} + return nil, nil, err + default: + } + + // any other errors are treated as recoverable and we try again after + // sending the event along the IncomingEvents channel + rtm.IncomingEvents <- RTMEvent{"connection_error", &ConnectionErrorEvent{ + Attempt: boff.attempts, + 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) + rtm.Debugln(" -> reconnecting in", dur) + time.Sleep(dur) + } +} + +// startRTMAndDial attempts to connect to the slack websocket. If useRTMStart is true, +// then it returns the full information returned by the "rtm.start" method on the +// slack API. Else it uses the "rtm.connect" method to connect +func (rtm *RTM) startRTMAndDial(useRTMStart bool) (info *Info, _ *websocket.Conn, err error) { + var ( + url string + ) + + 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 + } + + 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") + dialer := websocket.DefaultDialer + if rtm.dialer != nil { + dialer = rtm.dialer + } + conn, _, err := dialer.Dial(url, upgradeHeader) + if err != nil { + rtm.Debugf("Failed to dial to the websocket: %s", err) + return nil, nil, err + } + return info, conn, err +} + +// killConnection stops the websocket connection and signals to all goroutines +// that they should cease listening to the connection for events. +// +// This should not be called directly! Instead a boolean value (true for +// intentional, false otherwise) should be sent to the killChannel on the RTM. +func (rtm *RTM) killConnection(keepRunning chan bool, intentional bool) error { + rtm.Debugln("killing connection") + if rtm.isConnected { + close(keepRunning) + } + rtm.isConnected = false + rtm.wasIntentional = intentional + err := rtm.conn.Close() + rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{intentional}} + return err +} + +// handleEvents is a blocking function that handles all events. This sends +// pings when asked to (on rtm.forcePing) and upon every given elapsed +// interval. This also sends outgoing messages that are received from the RTM's +// outgoingMessages channel. This also handles incoming raw events from the RTM +// rawEvents channel. +func (rtm *RTM) handleEvents(keepRunning chan bool) { + ticker := time.NewTicker(rtm.pingInterval) + defer ticker.Stop() + for { + select { + // catch "stop" signal on channel close + case intentional := <-rtm.killChannel: + _ = rtm.killConnection(keepRunning, intentional) + return + + // detect when the connection is dead. + case <-rtm.pingDeadman.C: + rtm.Debugln("deadman switch trigger disconnecting") + _ = rtm.killConnection(keepRunning, false) + // send pings on ticker interval + case <-ticker.C: + err := rtm.ping() + if err != nil { + _ = rtm.killConnection(keepRunning, false) + return + } + case <-rtm.forcePing: + err := rtm.ping() + if err != nil { + _ = rtm.killConnection(keepRunning, false) + return + } + // listen for messages that need to be sent + case msg := <-rtm.outgoingMessages: + rtm.sendOutgoingMessage(msg) + // listen for incoming messages that need to be parsed + case rawEvent := <-rtm.rawEvents: + switch rtm.handleRawEvent(rawEvent) { + case rtmEventTypeGoodbye: + _ = rtm.killConnection(keepRunning, false) + default: + } + } + } +} + +// handleIncomingEvents monitors the RTM's opened websocket for any incoming +// events. It pushes the raw events onto the RTM channel rawEvents. +// +// This will stop executing once the RTM's keepRunning channel has been closed +// or has anything sent to it. +func (rtm *RTM) handleIncomingEvents(keepRunning <-chan bool) { + for { + // non-blocking listen to see if channel is closed + select { + // catch "stop" signal on channel close + case <-keepRunning: + return + default: + if err := rtm.receiveIncomingEvent(); err != nil { + return + } + } + } +} + +func (rtm *RTM) sendWithDeadline(msg interface{}) error { + // set a write deadline on the connection + if err := rtm.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { + return err + } + if err := rtm.conn.WriteJSON(msg); err != nil { + return err + } + // remove write deadline + return rtm.conn.SetWriteDeadline(time.Time{}) +} + +// sendOutgoingMessage sends the given OutgoingMessage to the slack websocket. +// +// It does not currently detect if a outgoing message fails due to a disconnect +// and instead lets a future failed 'PING' detect the failed connection. +func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) { + rtm.Debugln("Sending message:", msg) + if len(msg.Text) > MaxMessageTextLength { + rtm.IncomingEvents <- RTMEvent{"outgoing_error", &MessageTooLongEvent{ + Message: msg, + MaxLength: MaxMessageTextLength, + }} + return + } + + if err := rtm.sendWithDeadline(msg); err != nil { + rtm.IncomingEvents <- RTMEvent{"outgoing_error", &OutgoingErrorEvent{ + Message: msg, + ErrorObj: err, + }} + // TODO force ping? + } +} + +// ping sends a 'PING' message to the RTM's websocket. If the 'PING' message +// fails to send then this returns an error signifying that the connection +// should be considered disconnected. +// +// This does not handle incoming 'PONG' responses but does store the time of +// each successful 'PING' send so latency can be detected upon a 'PONG' +// response. +func (rtm *RTM) ping() error { + id := rtm.idGen.Next() + rtm.Debugln("Sending PING ", id) + msg := &Ping{ID: id, Type: "ping", Timestamp: time.Now().Unix()} + + if err := rtm.sendWithDeadline(msg); err != nil { + rtm.Debugf("RTM Error sending 'PING %d': %s", id, err.Error()) + return err + } + return nil +} + +// receiveIncomingEvent attempts to receive an event from the RTM's websocket. +// This will block until a frame is available from the websocket. +// If the read from the websocket results in a fatal error, this function will return non-nil. +func (rtm *RTM) receiveIncomingEvent() error { + event := json.RawMessage{} + err := rtm.conn.ReadJSON(&event) + switch { + case err == io.ErrUnexpectedEOF: + // 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 + // 'PING' message + + // trigger a 'PING' to detect potential websocket disconnect + rtm.forcePing <- true + case err != nil: + // All other errors from ReadJSON come from NextReader, and should + // kill the read loop and force a reconnect. + rtm.IncomingEvents <- RTMEvent{"incoming_error", &IncomingEventError{ + ErrorObj: err, + }} + rtm.killChannel <- false + return err + case len(event) == 0: + rtm.Debugln("Received empty event") + default: + rtm.Debugln("Incoming Event:", string(event[:])) + rtm.rawEvents <- event + } + return nil +} + +// handleRawEvent takes a raw JSON message received from the slack websocket +// and handles the encoded event. +// returns the event type of the message. +func (rtm *RTM) handleRawEvent(rawEvent json.RawMessage) string { + event := &Event{} + err := json.Unmarshal(rawEvent, event) + if err != nil { + rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} + return "" + } + + switch event.Type { + case rtmEventTypeAck: + rtm.handleAck(rawEvent) + case rtmEventTypeHello: + rtm.IncomingEvents <- RTMEvent{"hello", &HelloEvent{}} + case rtmEventTypePong: + rtm.handlePong(rawEvent) + case rtmEventTypeGoodbye: + // just return the event type up for goodbye, will be handled by caller. + case rtmEventTypeDesktopNotification: + rtm.Debugln("Received desktop notification, ignoring") + default: + rtm.handleEvent(event.Type, rawEvent) + } + + return event.Type +} + +// handleAck handles an incoming 'ACK' message. +func (rtm *RTM) handleAck(event json.RawMessage) { + ack := &AckMessage{} + if err := json.Unmarshal(event, ack); err != nil { + rtm.Debugln("RTM Error unmarshalling 'ack' event:", err) + 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{fmt.Errorf("ack decode failure")}} + } +} + +// handlePong handles an incoming 'PONG' message which should be in response to +// a previously sent 'PING' message. This is then used to compute the +// connection's latency. +func (rtm *RTM) handlePong(event json.RawMessage) { + var ( + p Pong + ) + + rtm.resetDeadman() + + if err := json.Unmarshal(event, &p); err != nil { + logger.Println("RTM Error unmarshalling 'pong' event:", err) + rtm.Debugln(" -> Erroneous 'ping' event:", string(event)) + return + } + + latency := time.Since(time.Unix(p.Timestamp, 0)) + rtm.IncomingEvents <- RTMEvent{"latency_report", &LatencyReport{Value: latency}} +} + +// handleEvent is the "default" response to an event that does not have a +// special case. It matches the command's name to a mapping of defined events +// and then sends the corresponding event struct to the IncomingEvents channel. +// If the event type is not found or the event cannot be unmarshalled into the +// correct struct then this sends an UnmarshallingErrorEvent to the +// IncomingEvents channel. +func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { + v, exists := EventMapping[typeStr] + if !exists { + rtm.Debugf("RTM Error, received unmapped event %q: %s\n", typeStr, string(event)) + err := fmt.Errorf("RTM Error: Received unmapped event %q: %s\n", typeStr, string(event)) + rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} + return + } + t := reflect.TypeOf(v) + recvEvent := reflect.New(t).Interface() + err := json.Unmarshal(event, recvEvent) + if err != nil { + rtm.Debugf("RTM Error, could not unmarshall event %q: %s\n", typeStr, string(event)) + err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s\n", typeStr, string(event)) + rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} + return + } + rtm.IncomingEvents <- RTMEvent{typeStr, recvEvent} +} + +// EventMapping holds a mapping of event names to their corresponding struct +// implementations. The structs should be instances of the unmarshalling +// target for the matching event type. +var EventMapping = map[string]interface{}{ + "message": MessageEvent{}, + "presence_change": PresenceChangeEvent{}, + "user_typing": UserTypingEvent{}, + + "channel_marked": ChannelMarkedEvent{}, + "channel_created": ChannelCreatedEvent{}, + "channel_joined": ChannelJoinedEvent{}, + "channel_left": ChannelLeftEvent{}, + "channel_deleted": ChannelDeletedEvent{}, + "channel_rename": ChannelRenameEvent{}, + "channel_archive": ChannelArchiveEvent{}, + "channel_unarchive": ChannelUnarchiveEvent{}, + "channel_history_changed": ChannelHistoryChangedEvent{}, + + "dnd_updated": DNDUpdatedEvent{}, + "dnd_updated_user": DNDUpdatedEvent{}, + + "im_created": IMCreatedEvent{}, + "im_open": IMOpenEvent{}, + "im_close": IMCloseEvent{}, + "im_marked": IMMarkedEvent{}, + "im_history_changed": IMHistoryChangedEvent{}, + + "group_marked": GroupMarkedEvent{}, + "group_open": GroupOpenEvent{}, + "group_joined": GroupJoinedEvent{}, + "group_left": GroupLeftEvent{}, + "group_close": GroupCloseEvent{}, + "group_rename": GroupRenameEvent{}, + "group_archive": GroupArchiveEvent{}, + "group_unarchive": GroupUnarchiveEvent{}, + "group_history_changed": GroupHistoryChangedEvent{}, + + "file_created": FileCreatedEvent{}, + "file_shared": FileSharedEvent{}, + "file_unshared": FileUnsharedEvent{}, + "file_public": FilePublicEvent{}, + "file_private": FilePrivateEvent{}, + "file_change": FileChangeEvent{}, + "file_deleted": FileDeletedEvent{}, + "file_comment_added": FileCommentAddedEvent{}, + "file_comment_edited": FileCommentEditedEvent{}, + "file_comment_deleted": FileCommentDeletedEvent{}, + + "pin_added": PinAddedEvent{}, + "pin_removed": PinRemovedEvent{}, + + "star_added": StarAddedEvent{}, + "star_removed": StarRemovedEvent{}, + + "reaction_added": ReactionAddedEvent{}, + "reaction_removed": ReactionRemovedEvent{}, + + "pref_change": PrefChangeEvent{}, + + "team_join": TeamJoinEvent{}, + "team_rename": TeamRenameEvent{}, + "team_pref_change": TeamPrefChangeEvent{}, + "team_domain_change": TeamDomainChangeEvent{}, + "team_migration_started": TeamMigrationStartedEvent{}, + + "manual_presence_change": ManualPresenceChangeEvent{}, + + "user_change": UserChangeEvent{}, + + "emoji_changed": EmojiChangedEvent{}, + + "commands_changed": CommandsChangedEvent{}, + + "email_domain_changed": EmailDomainChangedEvent{}, + + "bot_added": BotAddedEvent{}, + "bot_changed": BotChangedEvent{}, + + "accounts_changed": AccountsChangedEvent{}, + + "reconnect_url": ReconnectUrlEvent{}, + + "member_joined_channel": MemberJoinedChannelEvent{}, + "member_left_channel": MemberLeftChannelEvent{}, +} diff --git a/vendor/github.com/nlopes/slack/websocket_misc.go b/vendor/github.com/nlopes/slack/websocket_misc.go new file mode 100644 index 0000000..16f48c7 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_misc.go @@ -0,0 +1,140 @@ +package slack + +import ( + "encoding/json" + "fmt" +) + +// AckMessage is used for messages received in reply to other messages +type AckMessage struct { + ReplyTo int `json:"reply_to"` + Timestamp string `json:"ts"` + Text string `json:"text"` + RTMResponse +} + +// RTMResponse encapsulates response details as returned by the Slack API +type RTMResponse struct { + Ok bool `json:"ok"` + Error *RTMError `json:"error"` +} + +// RTMError encapsulates error information as returned by the Slack API +type RTMError struct { + Code int + Msg string +} + +func (s RTMError) Error() string { + return fmt.Sprintf("Code %d - %s", s.Code, s.Msg) +} + +// MessageEvent represents a Slack Message (used as the event type for an incoming message) +type MessageEvent Message + +// RTMEvent is the main wrapper. You will find all the other messages attached +type RTMEvent struct { + Type string + Data interface{} +} + +// HelloEvent represents the hello event +type HelloEvent struct{} + +// PresenceChangeEvent represents the presence change event +type PresenceChangeEvent struct { + Type string `json:"type"` + Presence string `json:"presence"` + User string `json:"user"` +} + +// UserTypingEvent represents the user typing event +type UserTypingEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` +} + +// PrefChangeEvent represents a user preferences change event +type PrefChangeEvent struct { + Type string `json:"type"` + Name string `json:"name"` + Value json.RawMessage `json:"value"` +} + +// ManualPresenceChangeEvent represents the manual presence change event +type ManualPresenceChangeEvent struct { + Type string `json:"type"` + Presence string `json:"presence"` +} + +// UserChangeEvent represents the user change event +type UserChangeEvent struct { + Type string `json:"type"` + User User `json:"user"` +} + +// EmojiChangedEvent represents the emoji changed event +type EmojiChangedEvent struct { + Type string `json:"type"` + SubType string `json:"subtype"` + Name string `json:"name"` + Names []string `json:"names"` + Value string `json:"value"` + EventTimestamp string `json:"event_ts"` +} + +// CommandsChangedEvent represents the commands changed event +type CommandsChangedEvent struct { + Type string `json:"type"` + EventTimestamp string `json:"event_ts"` +} + +// EmailDomainChangedEvent represents the email domain changed event +type EmailDomainChangedEvent struct { + Type string `json:"type"` + EventTimestamp string `json:"event_ts"` + EmailDomain string `json:"email_domain"` +} + +// BotAddedEvent represents the bot added event +type BotAddedEvent struct { + Type string `json:"type"` + Bot Bot `json:"bot"` +} + +// BotChangedEvent represents the bot changed event +type BotChangedEvent struct { + Type string `json:"type"` + Bot Bot `json:"bot"` +} + +// AccountsChangedEvent represents the accounts changed event +type AccountsChangedEvent struct { + Type string `json:"type"` +} + +// ReconnectUrlEvent represents the receiving reconnect url event +type ReconnectUrlEvent struct { + Type string `json:"type"` + URL string `json:"url"` +} + +// MemberJoinedChannelEvent, a user joined a public or private channel +type MemberJoinedChannelEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` + ChannelType string `json:"channel_type"` + Team string `json:"team"` + Inviter string `json:"inviter"` +} + +// MemberJoinedChannelEvent, a user left a public or private channel +type MemberLeftChannelEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` + ChannelType string `json:"channel_type"` + Team string `json:"team"` +} diff --git a/vendor/github.com/nlopes/slack/websocket_pins.go b/vendor/github.com/nlopes/slack/websocket_pins.go new file mode 100644 index 0000000..95445e2 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_pins.go @@ -0,0 +1,16 @@ +package slack + +type pinEvent struct { + Type string `json:"type"` + User string `json:"user"` + Item Item `json:"item"` + Channel string `json:"channel_id"` + EventTimestamp string `json:"event_ts"` + HasPins bool `json:"has_pins,omitempty"` +} + +// PinAddedEvent represents the Pin added event +type PinAddedEvent pinEvent + +// PinRemovedEvent represents the Pin removed event +type PinRemovedEvent pinEvent diff --git a/vendor/github.com/nlopes/slack/websocket_reactions.go b/vendor/github.com/nlopes/slack/websocket_reactions.go new file mode 100644 index 0000000..e497387 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_reactions.go @@ -0,0 +1,25 @@ +package slack + +// reactionItem is a lighter-weight item than is returned by the reactions list. +type reactionItem struct { + Type string `json:"type"` + Channel string `json:"channel,omitempty"` + File string `json:"file,omitempty"` + FileComment string `json:"file_comment,omitempty"` + Timestamp string `json:"ts,omitempty"` +} + +type reactionEvent struct { + Type string `json:"type"` + User string `json:"user"` + ItemUser string `json:"item_user"` + Item reactionItem `json:"item"` + Reaction string `json:"reaction"` + EventTimestamp string `json:"event_ts"` +} + +// ReactionAddedEvent represents the Reaction added event +type ReactionAddedEvent reactionEvent + +// ReactionRemovedEvent represents the Reaction removed event +type ReactionRemovedEvent reactionEvent diff --git a/vendor/github.com/nlopes/slack/websocket_stars.go b/vendor/github.com/nlopes/slack/websocket_stars.go new file mode 100644 index 0000000..e0f2dda --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_stars.go @@ -0,0 +1,14 @@ +package slack + +type starEvent struct { + Type string `json:"type"` + User string `json:"user"` + Item StarredItem `json:"item"` + EventTimestamp string `json:"event_ts"` +} + +// StarAddedEvent represents the Star added event +type StarAddedEvent starEvent + +// StarRemovedEvent represents the Star removed event +type StarRemovedEvent starEvent diff --git a/vendor/github.com/nlopes/slack/websocket_teams.go b/vendor/github.com/nlopes/slack/websocket_teams.go new file mode 100644 index 0000000..3898c83 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_teams.go @@ -0,0 +1,33 @@ +package slack + +// TeamJoinEvent represents the Team join event +type TeamJoinEvent struct { + Type string `json:"type"` + User User `json:"user"` +} + +// TeamRenameEvent represents the Team rename event +type TeamRenameEvent struct { + Type string `json:"type"` + Name string `json:"name,omitempty"` + EventTimestamp string `json:"event_ts,omitempty"` +} + +// TeamPrefChangeEvent represents the Team preference change event +type TeamPrefChangeEvent struct { + Type string `json:"type"` + Name string `json:"name,omitempty"` + Value []string `json:"value,omitempty"` +} + +// TeamDomainChangeEvent represents the Team domain change event +type TeamDomainChangeEvent struct { + Type string `json:"type"` + URL string `json:"url"` + Domain string `json:"domain"` +} + +// TeamMigrationStartedEvent represents the Team migration started event +type TeamMigrationStartedEvent struct { + Type string `json:"type"` +} diff --git a/vendor/github.com/pkg/errors/.gitignore b/vendor/github.com/pkg/errors/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/vendor/github.com/pkg/errors/.gitignore @@ -0,0 +1,24 @@ +# 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 diff --git a/vendor/github.com/pkg/errors/.travis.yml b/vendor/github.com/pkg/errors/.travis.yml new file mode 100644 index 0000000..588ceca --- /dev/null +++ b/vendor/github.com/pkg/errors/.travis.yml @@ -0,0 +1,11 @@ +language: go +go_import_path: github.com/pkg/errors +go: + - 1.4.3 + - 1.5.4 + - 1.6.2 + - 1.7.1 + - tip + +script: + - go test -v ./... diff --git a/vendor/github.com/pkg/errors/LICENSE b/vendor/github.com/pkg/errors/LICENSE new file mode 100644 index 0000000..835ba3e --- /dev/null +++ b/vendor/github.com/pkg/errors/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015, Dave Cheney +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/pkg/errors/README.md b/vendor/github.com/pkg/errors/README.md new file mode 100644 index 0000000..273db3c --- /dev/null +++ b/vendor/github.com/pkg/errors/README.md @@ -0,0 +1,52 @@ +# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) + +Package errors provides simple error handling primitives. + +`go get github.com/pkg/errors` + +The traditional error handling idiom in Go is roughly akin to +```go +if err != nil { + return err +} +``` +which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error. + +## Adding context to an error + +The errors.Wrap function returns a new error that adds context to the original error. For example +```go +_, err := ioutil.ReadAll(r) +if err != nil { + return errors.Wrap(err, "read failed") +} +``` +## Retrieving the cause of an error + +Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`. +```go +type causer interface { + Cause() error +} +``` +`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example: +```go +switch err := errors.Cause(err).(type) { +case *MyError: + // handle specifically +default: + // unknown error +} +``` + +[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors). + +## Contributing + +We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high. + +Before proposing a change, please discuss your change by raising an issue. + +## Licence + +BSD-2-Clause diff --git a/vendor/github.com/pkg/errors/appveyor.yml b/vendor/github.com/pkg/errors/appveyor.yml new file mode 100644 index 0000000..a932ead --- /dev/null +++ b/vendor/github.com/pkg/errors/appveyor.yml @@ -0,0 +1,32 @@ +version: build-{build}.{branch} + +clone_folder: C:\gopath\src\github.com\pkg\errors +shallow_clone: true # for startup speed + +environment: + GOPATH: C:\gopath + +platform: + - x64 + +# http://www.appveyor.com/docs/installed-software +install: + # some helpful output for debugging builds + - go version + - go env + # pre-installed MinGW at C:\MinGW is 32bit only + # but MSYS2 at C:\msys64 has mingw64 + - set PATH=C:\msys64\mingw64\bin;%PATH% + - gcc --version + - g++ --version + +build_script: + - go install -v ./... + +test_script: + - set PATH=C:\gopath\bin;%PATH% + - go test -v ./... + +#artifacts: +# - path: '%GOPATH%\bin\*.exe' +deploy: off diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go new file mode 100644 index 0000000..842ee80 --- /dev/null +++ b/vendor/github.com/pkg/errors/errors.go @@ -0,0 +1,269 @@ +// Package errors provides simple error handling primitives. +// +// The traditional error handling idiom in Go is roughly akin to +// +// if err != nil { +// return err +// } +// +// which applied recursively up the call stack results in error reports +// without context or debugging information. The errors package allows +// programmers to add context to the failure path in their code in a way +// that does not destroy the original value of the error. +// +// Adding context to an error +// +// The errors.Wrap function returns a new error that adds context to the +// original error by recording a stack trace at the point Wrap is called, +// and the supplied message. For example +// +// _, err := ioutil.ReadAll(r) +// if err != nil { +// return errors.Wrap(err, "read failed") +// } +// +// If additional control is required the errors.WithStack and errors.WithMessage +// functions destructure errors.Wrap into its component operations of annotating +// an error with a stack trace and an a message, respectively. +// +// Retrieving the cause of an error +// +// Using errors.Wrap constructs a stack of errors, adding context to the +// preceding error. Depending on the nature of the error it may be necessary +// to reverse the operation of errors.Wrap to retrieve the original error +// for inspection. Any error value which implements this interface +// +// type causer interface { +// Cause() error +// } +// +// can be inspected by errors.Cause. errors.Cause will recursively retrieve +// the topmost error which does not implement causer, which is assumed to be +// the original cause. For example: +// +// switch err := errors.Cause(err).(type) { +// case *MyError: +// // handle specifically +// default: +// // unknown error +// } +// +// causer interface is not exported by this package, but is considered a part +// of stable public API. +// +// Formatted printing of errors +// +// All error values returned from this package implement fmt.Formatter and can +// be formatted by the fmt package. The following verbs are supported +// +// %s print the error. If the error has a Cause it will be +// printed recursively +// %v see %s +// %+v extended format. Each Frame of the error's StackTrace will +// be printed in detail. +// +// Retrieving the stack trace of an error or wrapper +// +// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are +// invoked. This information can be retrieved with the following interface. +// +// type stackTracer interface { +// StackTrace() errors.StackTrace +// } +// +// Where errors.StackTrace is defined as +// +// type StackTrace []Frame +// +// The Frame type represents a call site in the stack trace. Frame supports +// the fmt.Formatter interface that can be used for printing information about +// the stack trace of this error. For example: +// +// if err, ok := err.(stackTracer); ok { +// for _, f := range err.StackTrace() { +// fmt.Printf("%+s:%d", f) +// } +// } +// +// stackTracer interface is not exported by this package, but is considered a part +// of stable public API. +// +// See the documentation for Frame.Format for more details. +package errors + +import ( + "fmt" + "io" +) + +// New returns an error with the supplied message. +// New also records the stack trace at the point it was called. +func New(message string) error { + return &fundamental{ + msg: message, + stack: callers(), + } +} + +// Errorf formats according to a format specifier and returns the string +// as a value that satisfies error. +// Errorf also records the stack trace at the point it was called. +func Errorf(format string, args ...interface{}) error { + return &fundamental{ + msg: fmt.Sprintf(format, args...), + stack: callers(), + } +} + +// fundamental is an error that has a message and a stack, but no caller. +type fundamental struct { + msg string + *stack +} + +func (f *fundamental) Error() string { return f.msg } + +func (f *fundamental) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, f.msg) + f.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, f.msg) + case 'q': + fmt.Fprintf(s, "%q", f.msg) + } +} + +// WithStack annotates err with a stack trace at the point WithStack was called. +// If err is nil, WithStack returns nil. +func WithStack(err error) error { + if err == nil { + return nil + } + return &withStack{ + err, + callers(), + } +} + +type withStack struct { + error + *stack +} + +func (w *withStack) Cause() error { return w.error } + +func (w *withStack) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v", w.Cause()) + w.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, w.Error()) + case 'q': + fmt.Fprintf(s, "%q", w.Error()) + } +} + +// Wrap returns an error annotating err with a stack trace +// at the point Wrap is called, and the supplied message. +// If err is nil, Wrap returns nil. +func Wrap(err error, message string) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: message, + } + return &withStack{ + err, + callers(), + } +} + +// Wrapf returns an error annotating err with a stack trace +// at the point Wrapf is call, and the format specifier. +// If err is nil, Wrapf returns nil. +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } + return &withStack{ + err, + callers(), + } +} + +// WithMessage annotates err with a new message. +// If err is nil, WithMessage returns nil. +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: message, + } +} + +type withMessage struct { + cause error + msg string +} + +func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } +func (w *withMessage) Cause() error { return w.cause } + +func (w *withMessage) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v\n", w.Cause()) + io.WriteString(s, w.msg) + return + } + fallthrough + case 's', 'q': + io.WriteString(s, w.Error()) + } +} + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + cause, ok := err.(causer) + if !ok { + break + } + err = cause.Cause() + } + return err +} diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go new file mode 100644 index 0000000..6b1f289 --- /dev/null +++ b/vendor/github.com/pkg/errors/stack.go @@ -0,0 +1,178 @@ +package errors + +import ( + "fmt" + "io" + "path" + "runtime" + "strings" +) + +// Frame represents a program counter inside a stack frame. +type Frame uintptr + +// pc returns the program counter for this frame; +// multiple frames may have the same PC value. +func (f Frame) pc() uintptr { return uintptr(f) - 1 } + +// file returns the full path to the file that contains the +// function for this Frame's pc. +func (f Frame) file() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + file, _ := fn.FileLine(f.pc()) + return file +} + +// line returns the line number of source code of the +// function for this Frame's pc. +func (f Frame) line() int { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return 0 + } + _, line := fn.FileLine(f.pc()) + return line +} + +// Format formats the frame according to the fmt.Formatter interface. +// +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+s path of source file relative to the compile time GOPATH +// %+v equivalent to %+s:%d +func (f Frame) Format(s fmt.State, verb rune) { + switch verb { + case 's': + switch { + case s.Flag('+'): + pc := f.pc() + fn := runtime.FuncForPC(pc) + if fn == nil { + io.WriteString(s, "unknown") + } else { + file, _ := fn.FileLine(pc) + fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file) + } + default: + io.WriteString(s, path.Base(f.file())) + } + case 'd': + fmt.Fprintf(s, "%d", f.line()) + case 'n': + name := runtime.FuncForPC(f.pc()).Name() + io.WriteString(s, funcname(name)) + case 'v': + f.Format(s, 's') + io.WriteString(s, ":") + f.Format(s, 'd') + } +} + +// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). +type StackTrace []Frame + +func (st StackTrace) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('+'): + for _, f := range st { + fmt.Fprintf(s, "\n%+v", f) + } + case s.Flag('#'): + fmt.Fprintf(s, "%#v", []Frame(st)) + default: + fmt.Fprintf(s, "%v", []Frame(st)) + } + case 's': + fmt.Fprintf(s, "%s", []Frame(st)) + } +} + +// stack represents a stack of program counters. +type stack []uintptr + +func (s *stack) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case st.Flag('+'): + for _, pc := range *s { + f := Frame(pc) + fmt.Fprintf(st, "\n%+v", f) + } + } + } +} + +func (s *stack) StackTrace() StackTrace { + f := make([]Frame, len(*s)) + for i := 0; i < len(f); i++ { + f[i] = Frame((*s)[i]) + } + return f +} + +func callers() *stack { + const depth = 32 + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + var st stack = pcs[0:n] + return &st +} + +// funcname removes the path prefix component of a function's name reported by func.Name(). +func funcname(name string) string { + i := strings.LastIndex(name, "/") + name = name[i+1:] + i = strings.Index(name, ".") + return name[i+1:] +} + +func trimGOPATH(name, file string) string { + // Here we want to get the source file path relative to the compile time + // GOPATH. As of Go 1.6.x there is no direct way to know the compiled + // GOPATH at runtime, but we can infer the number of path segments in the + // GOPATH. We note that fn.Name() returns the function name qualified by + // the import path, which does not include the GOPATH. Thus we can trim + // segments from the beginning of the file path until the number of path + // separators remaining is one more than the number of path separators in + // the function name. For example, given: + // + // GOPATH /home/user + // file /home/user/src/pkg/sub/file.go + // fn.Name() pkg/sub.Type.Method + // + // We want to produce: + // + // pkg/sub/file.go + // + // From this we can easily see that fn.Name() has one less path separator + // than our desired output. We count separators from the end of the file + // path until it finds two more than in the function name and then move + // one character forward to preserve the initial path segment without a + // leading separator. + const sep = "/" + goal := strings.Count(name, sep) + 2 + i := len(file) + for n := 0; n < goal; n++ { + i = strings.LastIndex(file[:i], sep) + if i == -1 { + // not enough separators found, set i so that the slice expression + // below leaves file unmodified + i = -len(sep) + break + } + } + // get back to 0 or trim the leading separator + file = file[i+len(sep):] + return file +}