diff --git a/Gopkg.lock b/Gopkg.lock index f5cfb64..40e8971 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -3,59 +3,93 @@ [[projects]] branch = "master" + digest = "1:592569a314f98130ac3085243fdbe46f278d3e54c95ce9e0bde9c6b908db82c4" name = "github.com/0xAX/notificator" packages = ["."] + pruneopts = "UT" revision = "88d57ee9043ba88d6a62e437fa15dda1ca0d2b59" [[projects]] + digest = "1:c2ee2bebf300b3c6d998802bdefe0422a65bcdcdd5c902e1ed518448c56e8f98" name = "github.com/erroneousboat/termui" packages = ["."] + pruneopts = "UT" revision = "80f245cdfa0488883a3e8602bf3f0c8a3c889a22" [[projects]] + digest = "1:cee8e8ac80df6373e7daa11baf1f98c1b6f7242c49ccae7e1ec34a971dc408d9" name = "github.com/gorilla/websocket" packages = ["."] - revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" - version = "v1.2.0" + pruneopts = "UT" + revision = "3ff3320c2a1756a3691521efc290b4701575147c" + version = "v1.3.0" [[projects]] + digest = "1:f614e627d47e1276989de725dc5e433504a8b5498850711c9d3fcec3bfa7c943" name = "github.com/maruel/panicparse" packages = ["stack"] - revision = "ad661195ed0e88491e0f14be6613304e3b1141d6" + pruneopts = "UT" + revision = "785840568bdc7faa0dfb1cd6c643207f03271f64" + version = "v1.1.1" [[projects]] + digest = "1:cdb899c199f907ac9fb50495ec71212c95cb5b0e0a8ee0800da0238036091033" name = "github.com/mattn/go-runewidth" packages = ["."] - revision = "9e777a8366cce605130a531d2cd6363d07ad7317" - version = "v0.0.2" + pruneopts = "UT" + revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb" + version = "v0.0.3" [[projects]] branch = "master" + digest = "1:e68cd472b96cdf7c9f6971ac41bcc1d4d3b23d67c2a31d2399446e295bc88ae9" name = "github.com/mitchellh/go-wordwrap" packages = ["."] + pruneopts = "UT" revision = "ad45545899c7b13c020ea92b2072220eefad42b8" [[projects]] + branch = "master" + digest = "1:410e126b7e96640ac0c41bb49bad7dbf2d1c081aa06fd2c75cdb9e65765fae9b" name = "github.com/nlopes/slack" packages = ["."] - revision = "8ab4d0b364ef1e9af5d102531da20d5ec902b6c4" - version = "v0.2.0" + pruneopts = "UT" + revision = "7cfa5619e6becd3db5dfb8e26c06798918e123b2" [[projects]] branch = "master" + digest = "1:f335d800550786b6f51ddaedb9d1107a7a72f4a2195e5b039dd7c0e103e119bc" name = "github.com/nsf/termbox-go" packages = ["."] - revision = "e2050e41c8847748ec5288741c0b19a8cb26d084" + pruneopts = "UT" + revision = "b66b20ab708e289ff1eb3e218478302e6aec28ce" [[projects]] + digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747" + name = "github.com/pkg/errors" + packages = ["."] + pruneopts = "UT" + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + +[[projects]] + digest = "1:3fd3d634f6815f19ac4b2c5e16d28ec9aa4584d0bba25d1ee6c424d813cca22a" name = "github.com/renstrom/fuzzysearch" packages = ["fuzzy"] - revision = "d4ca9dfccd55dc6b076f9880d49c35315922c1f4" - version = "v1.0.0" + pruneopts = "UT" + revision = "b18e754edff4833912ef4dce9eaca885bd3f0de1" + version = "v1.0.1" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "6cdfd0125aad6371a6f4e75c7fc29507cee4a6001a6c68e06c7237066a31153a" + input-imports = [ + "github.com/0xAX/notificator", + "github.com/erroneousboat/termui", + "github.com/mattn/go-runewidth", + "github.com/nlopes/slack", + "github.com/nsf/termbox-go", + "github.com/renstrom/fuzzysearch/fuzzy", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index f7cbb2b..b27b830 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -35,11 +35,11 @@ [[constraint]] name = "github.com/nlopes/slack" - version = "0.2.0" + branch = "master" [[constraint]] - branch = "master" name = "github.com/nsf/termbox-go" + branch = "master" [[constraint]] name = "github.com/renstrom/fuzzysearch" diff --git a/Makefile b/Makefile index fd9a149..c719243 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,12 @@ default: test # -timeout timout in seconds # -v verbose output test: - @echo "+ $@" + @ echo "+ $@" @ go test -timeout=5s -v +dev: build + @ ./bin/slack-term -debug + # `CGO_ENABLED=0` # Because of dynamically linked libraries, this will statically compile the # app with all libraries built in. You won't be able to cross-compile if CGO diff --git a/README.md b/README.md index b857d11..4454ff9 100644 --- a/README.md +++ b/README.md @@ -35,86 +35,13 @@ Setup 1. Get a slack token, click [here](https://api.slack.com/docs/oauth-test-tokens) 2. Create a `.slack-term` file, and place it in your home directory. Below is - an an example file, you can leave out the `OPTIONAL` parts, you are only - required to specify a `slack_token`. Remember that your file should be - a valid json file so don't forget to remove the comments. + an example of such a file. You are only required to specify a + `slack_token`. For more configuration options of the `.slack-term` file, + see the [wiki](https://github.com/erroneousboat/slack-term/wiki). ```javascript { - "slack_token": "yourslacktokenhere", - - // OPTIONAL: set the width of the sidebar (between 1 and 11), default is 1 - "sidebar_width": 1, - - // OPTIONAL: turn on desktop notifications for all incoming messages, set - // the value as: "all", and for only mentions and im messages set the - // value as: "mention", default is turned off: "" - "notify": "", - - // OPTIONAL: define custom key mappings, defaults are: - "key_map": { - "command": { - "i": "mode-insert", - "/": "mode-search", - "k": "channel-up", - "j": "channel-down", - "g": "channel-top", - "G": "channel-bottom", - "": "chat-up", - "C-b": "chat-up", - "C-u": "chat-up", - "": "chat-down", - "C-f": "chat-down", - "C-d": "chat-down", - "n": "channel-search-next", - "N": "channel-search-previous", - "q": "quit", - "": "help" - }, - "insert": { - "": "cursor-left", - "": "cursor-right", - "": "send", - "": "mode-command", - "": "backspace", - "C-8": "backspace", - "": "delete", - "": "space" - }, - "search": { - "": "cursor-left", - "": "cursor-right", - "": "clear-input", - "": "clear-input", - "": "backspace", - "C-8": "backspace", - "": "delete", - "": "space" - } - }, - - // OPTIONAL: override the default theme (see wiki for more information), - // defaults are: - "theme": { - "view": { - "fg": "white", - "bg": "default", - "border_fg": "white", - "border_bg": "white", - "par_fg": "white", - "par_label_fg": "white" - }, - "channel": { - "prefix": "", - "icon": "", - "text": "" - }, - "message": { - "time": "", - "name": "", - "text": "" - } - } + "slack_token": "yourslacktokenhere" } ``` @@ -128,19 +55,11 @@ command: $ slack-term ``` -You can also specify the location of the config file, this will give you -the possibility to run several instances of `slack-term` with different -accounts. - -```bash -$ slack-term -config [path-to-config-file] -``` - Default Key Mapping ------------------- -Below are the default key-mapping for `slack-term`, you can change them -in your `slack-term.json` file. +Below are the default key-mappings for `slack-term`, you can change them +in your `.slack-term` file. | mode | key | action | |---------|-----------|----------------------------| diff --git a/components/channels.go b/components/channels.go index b18e5d9..498822c 100644 --- a/components/channels.go +++ b/components/channels.go @@ -14,6 +14,7 @@ const ( IconChannel = "#" IconGroup = "☰" IconIM = "●" + IconMpIM = "☰" IconNotification = "*" PresenceAway = "away" @@ -22,6 +23,7 @@ const ( ChannelTypeChannel = "channel" ChannelTypeGroup = "group" ChannelTypeIM = "im" + ChannelTypeMpIM = "mpim" ) type ChannelItem struct { @@ -55,6 +57,8 @@ func (c ChannelItem) ToString() string { icon = IconChannel case ChannelTypeGroup: icon = IconGroup + case ChannelTypeMpIM: + icon = IconMpIM case ChannelTypeIM: switch c.Presence { case PresenceActive: @@ -93,6 +97,7 @@ func (c ChannelItem) GetChannelName() string { // Channels is the definition of a Channels component type Channels struct { + ChannelItems []ChannelItem List *termui.List SelectedChannel int // index of which channel is selected from the List Offset int // from what offset are channels rendered @@ -122,7 +127,7 @@ func CreateChannelsComponent(inputHeight int) *Channels { func (c *Channels) Buffer() termui.Buffer { buf := c.List.Buffer() - for i, item := range c.List.Items[c.Offset:] { + for i, item := range c.ChannelItems[c.Offset:] { y := c.List.InnerBounds().Min.Y + i @@ -134,12 +139,13 @@ func (c *Channels) Buffer() termui.Buffer { var cells []termui.Cell if y == c.CursorPosition { cells = termui.DefaultTxBuilder.Build( - item, c.List.ItemBgColor, c.List.ItemFgColor) + item.ToString(), c.List.ItemBgColor, c.List.ItemFgColor) } else { cells = termui.DefaultTxBuilder.Build( - item, c.List.ItemFgColor, c.List.ItemBgColor) + item.ToString(), c.List.ItemFgColor, c.List.ItemBgColor) } + // Append ellipsis when overflows cells = termui.DTrimTxCls(cells, c.List.InnerWidth()) x := 0 @@ -196,8 +202,33 @@ func (c *Channels) SetY(y int) { c.List.SetY(y) } -func (c *Channels) SetChannels(channels []string) { - c.List.Items = channels +func (c *Channels) SetChannels(channels []ChannelItem) { + c.ChannelItems = channels +} + +func (c *Channels) MarkAsRead(channelID int) { + c.ChannelItems[channelID].Notification = false +} + +func (c *Channels) MarkAsUnread(channelID string) { + index := c.FindChannel(channelID) + c.ChannelItems[index].Notification = true +} + +func (c *Channels) SetPresence(channelID string, presence string) { + index := c.FindChannel(channelID) + c.ChannelItems[index].Presence = presence +} + +func (c *Channels) FindChannel(channelID string) int { + var index int + for i, channel := range c.ChannelItems { + if channel.ID == channelID { + index = i + break + } + } + return index } // SetSelectedChannel sets the SelectedChannel given the index @@ -205,11 +236,6 @@ func (c *Channels) SetSelectedChannel(index int) { c.SelectedChannel = index } -// GetSelectedChannel returns the SelectedChannel -func (c *Channels) GetSelectedChannel() string { - return c.List.Items[c.SelectedChannel] -} - // MoveCursorUp will decrease the SelectedChannel by 1 func (c *Channels) MoveCursorUp() { if c.SelectedChannel > 0 { @@ -220,7 +246,7 @@ func (c *Channels) MoveCursorUp() { // MoveCursorDown will increase the SelectedChannel by 1 func (c *Channels) MoveCursorDown() { - if c.SelectedChannel < len(c.List.Items)-1 { + if c.SelectedChannel < len(c.ChannelItems)-1 { c.SetSelectedChannel(c.SelectedChannel + 1) c.ScrollDown() } @@ -235,9 +261,9 @@ func (c *Channels) MoveCursorTop() { // MoveCursorBottom will move the cursor to the bottom of the channels func (c *Channels) MoveCursorBottom() { - c.SetSelectedChannel(len(c.List.Items) - 1) + c.SetSelectedChannel(len(c.ChannelItems) - 1) - offset := len(c.List.Items) - (c.List.InnerBounds().Max.Y - 1) + offset := len(c.ChannelItems) - (c.List.InnerBounds().Max.Y - 1) if offset < 0 { c.Offset = 0 @@ -264,7 +290,7 @@ func (c *Channels) ScrollUp() { func (c *Channels) ScrollDown() { // Is the cursor at the bottom of the channel view? if c.CursorPosition == c.List.InnerBounds().Max.Y-1 { - if c.Offset < len(c.List.Items)-1 { + if c.Offset < len(c.ChannelItems)-1 { c.Offset++ } } else { @@ -278,11 +304,16 @@ func (c *Channels) ScrollDown() { func (c *Channels) Search(term string) { c.SearchMatches = make([]int, 0) - matches := fuzzy.Find(term, c.List.Items) + targets := make([]string, 0) + for _, c := range c.ChannelItems { + targets = append(targets, c.Name) + } + + matches := fuzzy.Find(term, targets) for _, m := range matches { - for i, item := range c.List.Items { - if m == item { + for i, item := range c.ChannelItems { + if m == item.Name { c.SearchMatches = append(c.SearchMatches, i) break } diff --git a/components/chat.go b/components/chat.go index 5dd2b93..9e8d73a 100644 --- a/components/chat.go +++ b/components/chat.go @@ -2,16 +2,29 @@ package components import ( "fmt" - "html" "sort" "strings" "time" "github.com/erroneousboat/termui" + runewidth "github.com/mattn/go-runewidth" "github.com/erroneousboat/slack-term/config" ) +var ( + COLORS = []string{ + "fg-black", + "fg-red", + "fg-green", + "fg-yellow", + "fg-blue", + "fg-magenta", + "fg-cyan", + "fg-white", + } +) + type Message struct { Time time.Time Name string @@ -20,33 +33,30 @@ type Message struct { StyleTime string StyleName string StyleText string + + FormatTime string } -func (m Message) ToString() string { - if (m.Time != time.Time{} && m.Name != "") { +func (m Message) colorizeName(styleName string) string { + if strings.Contains(styleName, "colorize") { + var sum int + for _, c := range m.Name { + sum = sum + int(c) + } - return html.UnescapeString( - fmt.Sprintf( - "[[%s]](%s) [<%s>](%s) [%s](%s)", - m.Time.Format("15:04"), - m.StyleTime, - m.Name, - m.StyleName, - m.Content, - m.StyleText, - ), - ) - } else { - return html.UnescapeString( - fmt.Sprintf("[%s](%s)", m.Content, m.StyleText), - ) + i := sum % len(COLORS) + + return strings.Replace(m.StyleName, "colorize", COLORS[i], -1) } + + return styleName } // Chat is the definition of a Chat component type Chat struct { - List *termui.List - Offset int + List *termui.List + Messages []Message + Offset int } // CreateChat is the constructor for the Chat struct @@ -64,11 +74,59 @@ func CreateChatComponent(inputHeight int) *Chat { // Buffer implements interface termui.Bufferer func (c *Chat) Buffer() termui.Buffer { - // Build cells, after every item put a newline - cells := termui.DefaultTxBuilder.Build( - strings.Join(c.List.Items, "\n"), - c.List.ItemFgColor, c.List.ItemBgColor, - ) + // Build cells. We're building parts of the message individually, or else + // DefaultTxBuilder will interpret potential markdown usage in a message + // as well. + cells := make([]termui.Cell, 0) + for i, msg := range c.Messages { + + // When msg.Time and msg.Name are empty (in the case of attachments) + // don't add the time and name parts. + if (msg.Time != time.Time{} && msg.Name != "") { + // Time + cells = append(cells, termui.DefaultTxBuilder.Build( + fmt.Sprintf( + "[[%s]](%s) ", + msg.Time.Format(msg.FormatTime), + msg.StyleTime, + ), + termui.ColorDefault, termui.ColorDefault)..., + ) + + // Name + cells = append(cells, termui.DefaultTxBuilder.Build( + fmt.Sprintf("[<%s>](%s) ", + msg.Name, + msg.colorizeName(msg.StyleName), + ), + termui.ColorDefault, termui.ColorDefault)..., + ) + } + + // Hack, in order to get the correct fg and bg attributes. This is + // because the readAttr function in termui is unexported. + txCells := termui.DefaultTxBuilder.Build( + fmt.Sprintf("[.](%s)", msg.StyleText), + termui.ColorDefault, termui.ColorDefault, + ) + + // Text + for _, r := range msg.Content { + cells = append( + cells, + termui.Cell{ + Ch: r, + Fg: txCells[0].Fg, + Bg: txCells[0].Bg, + }, + ) + } + + // Add a newline after every message + if i < len(c.Messages)-1 { + cells = append(cells, termui.Cell{Ch: '\n'}) + } + } // We will create an array of Line structs, this allows us // to more easily render the items in a list. We will range @@ -81,7 +139,7 @@ func (c *Chat) Buffer() termui.Buffer { lines := []Line{} line := Line{} - // When we encounter a newline or are at the bounds of the chat view we + // When we encounter a newline or, are at the bounds of the chat view we // stop iterating over the cells and add the line to the line array x := 0 for _, cell := range cells { @@ -105,7 +163,7 @@ func (c *Chat) Buffer() termui.Buffer { } line.cells = append(line.cells, cell) - x++ + x += cell.Width() } // Append the last line to the array when we didn't encounter any @@ -145,7 +203,7 @@ func (c *Chat) Buffer() termui.Buffer { Bg: c.List.ItemBgColor, }, ) - x++ + x += runewidth.RuneWidth(' ') } currentY-- } @@ -164,7 +222,7 @@ func (c *Chat) Buffer() termui.Buffer { Bg: c.List.ItemBgColor, }, ) - x++ + x += runewidth.RuneWidth(' ') } currentY-- } @@ -198,26 +256,23 @@ func (c *Chat) GetMaxItems() int { return c.List.InnerBounds().Max.Y - c.List.InnerBounds().Min.Y } -// SetMessages will put the provided messages into the Items field of the +// SetMessages will put the provided messages into the Messages field of the // Chat view -func (c *Chat) SetMessages(messages []string) { +func (c *Chat) SetMessages(messages []Message) { // Reset offset first, when scrolling in view and changing channels we // want the offset to be 0 when loading new messages c.Offset = 0 - - for _, msg := range messages { - c.List.Items = append(c.List.Items, html.UnescapeString(msg)) - } + c.Messages = messages } -// AddMessage adds a single message to List.Items -func (c *Chat) AddMessage(message string) { - c.List.Items = append(c.List.Items, html.UnescapeString(message)) +// AddMessage adds a single message to Messages +func (c *Chat) AddMessage(message Message) { + c.Messages = append(c.Messages, message) } -// ClearMessages clear the List.Items +// ClearMessages clear the c.Messages func (c *Chat) ClearMessages() { - c.List.Items = []string{} + c.Messages = make([]Message, 0) } // ScrollUp will render the chat messages based on the Offset of the Chat @@ -226,13 +281,13 @@ func (c *Chat) ClearMessages() { // Offset is 0 when scrolled down. (we loop backwards over the array, so we // start with rendering last item in the list at the maximum y of the Chat // pane). Increasing the Offset will thus result in substracting the offset -// from the len(Chat.List.Items). +// from the len(Chat.Messages). func (c *Chat) ScrollUp() { c.Offset = c.Offset + 10 // Protect overscrolling - if c.Offset > len(c.List.Items) { - c.Offset = len(c.List.Items) + if c.Offset > len(c.Messages) { + c.Offset = len(c.Messages) } } @@ -242,7 +297,7 @@ func (c *Chat) ScrollUp() { // Offset is 0 when scrolled down. (we loop backwards over the array, so we // start with rendering last item in the list at the maximum y of the Chat // pane). Increasing the Offset will thus result in substracting the offset -// from the len(Chat.List.Items). +// from the len(Chat.Messages). func (c *Chat) ScrollDown() { c.Offset = c.Offset - 10 @@ -258,20 +313,22 @@ func (c *Chat) SetBorderLabel(channelName string) { } // Help shows the usage and key bindings in the chat pane -func (c *Chat) Help(cfg *config.Config) { - help := []string{ - "slack-term - slack client for your terminal", - "", - "USAGE:", - " slack-term -config [path-to-config]", - "", - "KEY BINDINGS:", - "", +func (c *Chat) Help(usage string, cfg *config.Config) { + help := []Message{ + Message{ + Content: usage, + }, } for mode, mapping := range cfg.KeyMap { - help = append(help, fmt.Sprintf(" %s", strings.ToUpper(mode))) - help = append(help, "") + help = append( + help, + Message{ + Content: fmt.Sprintf("%s", strings.ToUpper(mode)), + }, + ) + + help = append(help, Message{Content: ""}) var keys []string for k := range mapping { @@ -280,10 +337,16 @@ func (c *Chat) Help(cfg *config.Config) { sort.Strings(keys) for _, k := range keys { - help = append(help, fmt.Sprintf(" %-12s%-15s", k, mapping[k])) + help = append( + help, + Message{ + Content: fmt.Sprintf(" %-12s%-15s", k, mapping[k]), + }, + ) } - help = append(help, "") + + help = append(help, Message{Content: ""}) } - c.List.Items = help + c.Messages = help } diff --git a/config/config.go b/config/config.go index abde6e4..99efffd 100644 --- a/config/config.go +++ b/config/config.go @@ -18,6 +18,7 @@ const ( type Config struct { SlackToken string `json:"slack_token"` Notify string `json:"notify"` + Emoji bool `json:"emoji"` SidebarWidth int `json:"sidebar_width"` MainWidth int `json:"-"` KeyMap map[string]keyMapping `json:"key_map"` @@ -53,12 +54,12 @@ func NewConfig(filepath string) (*Config, error) { } termui.ColorMap = map[string]termui.Attribute{ - "fg": termui.StringToAttribute(cfg.Theme.View.Fg), - "bg": termui.StringToAttribute(cfg.Theme.View.Bg), - "border.fg": termui.StringToAttribute(cfg.Theme.View.BorderFg), - "label.fg": termui.StringToAttribute(cfg.Theme.View.LabelFg), - "par.fg": termui.StringToAttribute(cfg.Theme.View.ParFg), - "par.label.bg": termui.StringToAttribute(cfg.Theme.View.ParLabelFg), + "fg": termui.StringToAttribute(cfg.Theme.View.Fg), + "bg": termui.StringToAttribute(cfg.Theme.View.Bg), + "border.fg": termui.StringToAttribute(cfg.Theme.View.BorderFg), + "border.bg": termui.StringToAttribute(cfg.Theme.View.BorderBg), + "label.fg": termui.StringToAttribute(cfg.Theme.View.LabelFg), + "label.bg": termui.StringToAttribute(cfg.Theme.View.LabelBg), } return &cfg, nil @@ -69,6 +70,7 @@ func getDefaultConfig() Config { SidebarWidth: 1, MainWidth: 11, Notify: "", + Emoji: false, KeyMap: map[string]keyMapping{ "command": { "i": "mode-insert", @@ -111,12 +113,12 @@ func getDefaultConfig() Config { }, Theme: Theme{ View: View{ - Fg: "white", - Bg: "default", - BorderFg: "white", - LabelFg: "green,bold", - ParFg: "white", - ParLabelFg: "white", + Fg: "white", + Bg: "default", + BorderFg: "white", + BorderBg: "", + LabelFg: "green,bold", + LabelBg: "", }, Channel: Channel{ Prefix: "", @@ -124,9 +126,10 @@ func getDefaultConfig() Config { Text: "", }, Message: Message{ - Time: "", - Name: "", - Text: "", + Time: "", + TimeFormat: "15:04", + Name: "", + Text: "", }, }, } diff --git a/config/theme.go b/config/theme.go index 0b113d9..4e2e73c 100644 --- a/config/theme.go +++ b/config/theme.go @@ -7,18 +7,19 @@ type Theme struct { } type View struct { - Fg string `json:"fg"` - Bg string `json:"bg"` - BorderFg string `json:"border_fg"` - LabelFg string `json:"border_fg"` - ParFg string `json:"par_fg"` - ParLabelFg string `json:"par_label_fg"` + Fg string `json:"fg"` // Foreground text + Bg string `json:"bg"` // Background text + BorderFg string `json:"border_fg"` // Border foreground + BorderBg string `json:"border_bg"` // Border background + LabelFg string `json:"label_fg"` // Label text foreground + LabelBg string `json:"label_bg"` // Label text background } type Message struct { - Time string `json:"time"` - Name string `json:"name"` - Text string `json:"text"` + Time string `json:"time"` + Name string `json:"name"` + Text string `json:"text"` + TimeFormat string `json:"time_format"` } type Channel struct { diff --git a/context/context.go b/context/context.go index ffad4f9..91447a6 100644 --- a/context/context.go +++ b/context/context.go @@ -1,6 +1,7 @@ package context import ( + "errors" "net/http" _ "net/http/pprof" "os" @@ -21,6 +22,8 @@ const ( ) type AppContext struct { + Version string + Usage string EventQueue chan termbox.Event Service *service.SlackService Body *termui.Grid @@ -33,7 +36,7 @@ type AppContext struct { // CreateAppContext creates an application context which can be passed // and referenced througout the application -func CreateAppContext(flgConfig string, flgToken string, flgDebug bool) (*AppContext, error) { +func CreateAppContext(flgConfig string, flgToken string, flgDebug bool, version string, usage string) (*AppContext, error) { if flgDebug { go func() { http.ListenAndServe(":6060", nil) @@ -59,6 +62,17 @@ func CreateAppContext(flgConfig string, flgToken string, flgDebug bool) (*AppCon } } + // Create desktop notifier + var notify *notificator.Notificator + if config.Notify != "" { + notify = notificator.New(notificator.Options{AppName: "slack-term"}) + if notify == nil { + return nil, errors.New( + "desktop notifications are not supported for your OS", + ) + } + } + // Create Service svc, err := service.NewSlackService(config) if err != nil { @@ -66,7 +80,10 @@ func CreateAppContext(flgConfig string, flgToken string, flgDebug bool) (*AppCon } // Create the main view - view := views.CreateView(config, svc) + view, err := views.CreateView(config, svc) + if err != nil { + return nil, err + } // Setup the interface if flgDebug { @@ -98,6 +115,8 @@ func CreateAppContext(flgConfig string, flgToken string, flgDebug bool) (*AppCon termui.Render(termui.Body) return &AppContext{ + Version: version, + Usage: usage, EventQueue: make(chan termbox.Event, 20), Service: svc, Body: termui.Body, @@ -105,6 +124,6 @@ func CreateAppContext(flgConfig string, flgToken string, flgDebug bool) (*AppCon Config: config, Debug: flgDebug, Mode: CommandMode, - Notify: notificator.New(notificator.Options{AppName: "slack-term"}), + Notify: notify, }, nil } diff --git a/handlers/event.go b/handlers/event.go index 4d5473f..f2ac846 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -2,8 +2,11 @@ package handlers import ( "fmt" + "log" "os" + "regexp" "strconv" + "strings" "time" "github.com/0xAX/notificator" @@ -11,6 +14,7 @@ import ( "github.com/nlopes/slack" termbox "github.com/nsf/termbox-go" + "github.com/erroneousboat/slack-term/components" "github.com/erroneousboat/slack-term/config" "github.com/erroneousboat/slack-term/context" "github.com/erroneousboat/slack-term/views" @@ -115,13 +119,13 @@ func messageHandler(ctx *context.AppContext) { } // Add message to the selected channel - if ev.Channel == ctx.Service.Channels[ctx.View.Channels.SelectedChannel].ID { + if ev.Channel == ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID { // Reverse order of messages, mainly done // when attachments are added to message for i := len(msg) - 1; i >= 0; i-- { ctx.View.Chat.AddMessage( - msg[i].ToString(), + msg[i], ) } @@ -141,6 +145,10 @@ func messageHandler(ctx *context.AppContext) { } case *slack.PresenceChangeEvent: actionSetPresence(ctx, ev.User, ev.Presence) + case *slack.RTMError: + ctx.View.Debug.Println( + ev.Error(), + ) } } } @@ -236,14 +244,22 @@ func actionSend(ctx *context.AppContext) { ctx.View.Refresh() // Send message - ctx.Service.SendMessage( - ctx.View.Channels.SelectedChannel, + err := ctx.Service.SendMessage( + ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID, message, ) + if err != nil { + ctx.View.Debug.Println( + err.Error(), + ) + } // Clear notification icon if there is any - ctx.Service.MarkAsRead(ctx.View.Channels.SelectedChannel) - ctx.View.Channels.SetChannels(ctx.Service.ChannelsToString()) + channelItem := ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel] + if channelItem.Notification { + ctx.Service.MarkAsRead(channelItem.ID) + ctx.View.Channels.MarkAsRead(ctx.View.Channels.SelectedChannel) + } termui.Render(ctx.View.Channels) } } @@ -294,17 +310,17 @@ func actionSearchMode(ctx *context.AppContext) { } func actionGetMessages(ctx *context.AppContext) { - msgs := ctx.Service.GetMessages( - ctx.Service.Channels[ctx.View.Channels.SelectedChannel], + msgs, err := ctx.Service.GetMessages( + ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID, ctx.View.Chat.GetMaxItems(), ) - - var strMsgs []string - for _, msg := range msgs { - strMsgs = append(strMsgs, msg.ToString()) + if err != nil { + termbox.Close() + log.Println(err) + os.Exit(0) } - ctx.View.Chat.SetMessages(strMsgs) + ctx.View.Chat.SetMessages(msgs) termui.Render(ctx.View.Chat) } @@ -375,27 +391,30 @@ func actionChangeChannel(ctx *context.AppContext) { // Get messages of the SelectedChannel, and get the count of messages // that fit into the Chat component - msgs := ctx.Service.GetMessages( - ctx.Service.GetSlackChannel(ctx.View.Channels.SelectedChannel), + msgs, err := ctx.Service.GetMessages( + ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID, ctx.View.Chat.GetMaxItems(), ) - - var strMsgs []string - for _, msg := range msgs { - strMsgs = append(strMsgs, msg.ToString()) + if err != nil { + termbox.Close() + log.Println(err) + os.Exit(0) } // Set messages for the channel - ctx.View.Chat.SetMessages(strMsgs) + ctx.View.Chat.SetMessages(msgs) // Set channel name for the Chat pane ctx.View.Chat.SetBorderLabel( - ctx.Service.Channels[ctx.View.Channels.SelectedChannel].GetChannelName(), + ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].GetChannelName(), ) // Clear notification icon if there is any - ctx.Service.MarkAsRead(ctx.View.Channels.SelectedChannel) - ctx.View.Channels.SetChannels(ctx.Service.ChannelsToString()) + channelItem := ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel] + if channelItem.Notification { + ctx.Service.MarkAsRead(channelItem.ID) + ctx.View.Channels.MarkAsRead(ctx.View.Channels.SelectedChannel) + } termui.Render(ctx.View.Channels) termui.Render(ctx.View.Chat) @@ -404,8 +423,7 @@ func actionChangeChannel(ctx *context.AppContext) { // actionNewMessage will set the new message indicator for a channel, and // if configured will also display a desktop notification func actionNewMessage(ctx *context.AppContext, ev *slack.MessageEvent) { - ctx.Service.MarkAsUnread(ev.Channel) - ctx.View.Channels.SetChannels(ctx.Service.ChannelsToString()) + ctx.View.Channels.MarkAsUnread(ev.Channel) termui.Render(ctx.View.Channels) // Terminal bell @@ -413,7 +431,7 @@ func actionNewMessage(ctx *context.AppContext, ev *slack.MessageEvent) { // Desktop notification if ctx.Config.Notify == config.NotifyMention { - if ctx.Service.CheckNotifyMention(ev) { + if isMention(ctx, ev) { createNotifyMessage(ctx, ev) } } else if ctx.Config.Notify == config.NotifyAll { @@ -422,8 +440,7 @@ func actionNewMessage(ctx *context.AppContext, ev *slack.MessageEvent) { } func actionSetPresence(ctx *context.AppContext, channelID string, presence string) { - ctx.Service.SetPresenceChannelEvent(channelID, presence) - ctx.View.Channels.SetChannels(ctx.Service.ChannelsToString()) + ctx.View.Channels.SetPresence(channelID, presence) termui.Render(ctx.View.Channels) } @@ -438,7 +455,7 @@ func actionScrollDownChat(ctx *context.AppContext) { } func actionHelp(ctx *context.AppContext) { - ctx.View.Chat.Help(ctx.Config) + ctx.View.Chat.Help(ctx.Usage, ctx.Config) termui.Render(ctx.View.Chat) } @@ -492,20 +509,52 @@ func getKeyString(e termbox.Event) string { return ek } +// isMention check if the message event either contains a +// mention or is posted on an IM channel. +func isMention(ctx *context.AppContext, ev *slack.MessageEvent) bool { + channel := ctx.View.Channels.ChannelItems[ctx.View.Channels.FindChannel(ev.Channel)] + + if channel.Type == components.ChannelTypeIM { + return true + } + + // Mentions have the following format: + // <@U12345|erroneousboat> + // <@U12345> + r := regexp.MustCompile(`\<@(\w+\|*\w+)\>`) + matches := r.FindAllString(ev.Text, -1) + for _, match := range matches { + if strings.Contains(match, ctx.Service.CurrentUserID) { + return true + } + } + + return false +} + func createNotifyMessage(ctx *context.AppContext, ev *slack.MessageEvent) { go func() { if notifyTimer != nil { notifyTimer.Stop() } + // Only actually notify when time expires notifyTimer = time.NewTimer(time.Second * 2) <-notifyTimer.C - // Only actually notify when time expires - ctx.Notify.Push( - "slack-term", - ctx.Service.CreateNotifyMessage(ev.Channel), "", - notificator.UR_NORMAL, - ) + var message string + channel := ctx.View.Channels.ChannelItems[ctx.View.Channels.FindChannel(ev.Channel)] + switch channel.Type { + case components.ChannelTypeChannel: + message = fmt.Sprintf("Message received on channel: %s", channel.Name) + case components.ChannelTypeGroup: + message = fmt.Sprintf("Message received in group: %s", channel.Name) + case components.ChannelTypeIM: + message = fmt.Sprintf("Message received from: %s", channel.Name) + default: + message = fmt.Sprintf("Message received from: %s", channel.Name) + } + + ctx.Notify.Push("slack-term", message, "", notificator.UR_NORMAL) }() } diff --git a/main.go b/main.go index 3784098..3f92141 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( ) const ( - VERSION = "v0.4.0" + VERSION = "v0.4.1" USAGE = `NAME: slack-term - slack client for your terminal @@ -30,7 +30,10 @@ WEBSITE: https://github.com/erroneousboat/slack-term GLOBAL OPTIONS: - --help, -h + -config [path-to-config-file] + -token [slack-token] + -debug + -help, -h ` ) @@ -95,7 +98,10 @@ func main() { termui.DefaultEvtStream = customEvtStream // Create context - ctx, err := context.CreateAppContext(flgConfig, flgToken, flgDebug) + usage := fmt.Sprintf(USAGE, VERSION) + ctx, err := context.CreateAppContext( + flgConfig, flgToken, flgDebug, VERSION, usage, + ) if err != nil { termbox.Close() log.Println(err) diff --git a/screenshot.png b/screenshot.png index a95f95e..7fada4e 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/service/slack.go b/service/slack.go index 7ef28c2..9e82d90 100644 --- a/service/slack.go +++ b/service/slack.go @@ -3,8 +3,8 @@ package service import ( "errors" "fmt" - "log" "regexp" + "sort" "strconv" "strings" "sync" @@ -16,18 +16,11 @@ import ( "github.com/erroneousboat/slack-term/config" ) -const ( - ChannelTypeChannel = "channel" - ChannelTypeGroup = "group" - ChannelTypeIM = "im" -) - type SlackService struct { Config *config.Config Client *slack.Client RTM *slack.RTM - SlackChannels []interface{} - Channels []components.ChannelItem + Conversations []slack.Channel UserCache map[string]string CurrentUserID string CurrentUsername string @@ -75,176 +68,174 @@ func NewSlackService(config *config.Config) (*SlackService, error) { return svc, nil } -// GetChannels will retrieve all available channels, groups, and im channels. -// Because the channels are of different types, we will append them to -// an []interface as well as to a []Channel which will give us easy access -// to the id and name of the Channel. -func (s *SlackService) GetChannels() []string { - var chans []components.ChannelItem +func (s *SlackService) GetChannels() ([]components.ChannelItem, error) { + slackChans := make([]slack.Channel, 0) - var wg sync.WaitGroup - - // Channels - wg.Add(1) - var slackChans []slack.Channel - go func() { - var err error - slackChans, err = s.Client.GetChannels(true) - if err != nil { - chans = append(chans, components.ChannelItem{}) - } - wg.Done() - }() - - // Groups - wg.Add(1) - var slackGroups []slack.Group - go func() { - var err error - slackGroups, err = s.Client.GetGroups(true) - if err != nil { - chans = append(chans, components.ChannelItem{}) - } - wg.Done() - }() - - // IM - wg.Add(1) - var slackIM []slack.IM - go func() { - var err error - slackIM, err = s.Client.GetIMChannels() - if err != nil { - chans = append(chans, components.ChannelItem{}) - } - wg.Done() - }() - - wg.Wait() - - // Channels - for _, chn := range slackChans { - if chn.IsMember { - s.SlackChannels = append(s.SlackChannels, chn) - chans = append( - chans, components.ChannelItem{ - ID: chn.ID, - Name: chn.Name, - Topic: chn.Topic.Value, - Type: components.ChannelTypeChannel, - UserID: "", - StylePrefix: s.Config.Theme.Channel.Prefix, - StyleIcon: s.Config.Theme.Channel.Icon, - StyleText: s.Config.Theme.Channel.Text, - }, - ) - } + // Initial request + initChans, initCur, err := s.Client.GetConversations( + &slack.GetConversationsParameters{ + ExcludeArchived: "true", + Limit: 10, + Types: []string{ + "public_channel", + "private_channel", + "im", + "mpim", + }, + }, + ) + if err != nil { + return nil, err } - // Groups - for _, grp := range slackGroups { - s.SlackChannels = append(s.SlackChannels, grp) - chans = append( - chans, components.ChannelItem{ - ID: grp.ID, - Name: grp.Name, - Topic: grp.Topic.Value, - Type: components.ChannelTypeGroup, - UserID: "", - StylePrefix: s.Config.Theme.Channel.Prefix, - StyleIcon: s.Config.Theme.Channel.Icon, - StyleText: s.Config.Theme.Channel.Text, + slackChans = append(slackChans, initChans...) + + // Paginate over additional channels + nextCur := initCur + for nextCur != "" { + channels, cursor, err := s.Client.GetConversations( + &slack.GetConversationsParameters{ + Cursor: nextCur, + ExcludeArchived: "true", + Limit: 10, + Types: []string{ + "public_channel", + "private_channel", + "im", + "mpim", + }, }, ) - } - - // IM - for _, im := range slackIM { - - // Uncover name, when we can't uncover name for - // IM channel this is then probably a deleted - // user, because we won't add deleted users - // to the UserCache, so we skip it - name, ok := s.UserCache[im.User] - - if ok { - chans = append( - chans, - components.ChannelItem{ - ID: im.ID, - Name: name, - Topic: "", - Type: components.ChannelTypeIM, - UserID: im.User, - Presence: "", - StylePrefix: s.Config.Theme.Channel.Prefix, - StyleIcon: s.Config.Theme.Channel.Icon, - StyleText: s.Config.Theme.Channel.Text, - }, - ) - s.SlackChannels = append(s.SlackChannels, im) + if err != nil { + return nil, err } + + slackChans = append(slackChans, channels...) + nextCur = cursor } - s.Channels = chans - - // We set presence of IM channels here because we need to separately - // issue an API call for every channel, this will speed up that process - s.SetPresenceChannels() - - var channels []string - for _, chn := range s.Channels { - channels = append(channels, chn.ToString()) + // We're creating tempChan, because we want to be able to + // sort the types of channels into buckets + type tempChan struct { + channelItem components.ChannelItem + slackChannel slack.Channel } - return channels -} + // Initialize buckets + buckets := make(map[int]map[string]*tempChan) + buckets[0] = make(map[string]*tempChan) // Channels + buckets[1] = make(map[string]*tempChan) // Group + buckets[2] = make(map[string]*tempChan) // MpIM + buckets[3] = make(map[string]*tempChan) // IM -// ChannelsToString will relay the string representation for a channel -func (s *SlackService) ChannelsToString() []string { - var channels []string - for _, chn := range s.Channels { - channels = append(channels, chn.ToString()) - } - return channels -} - -// SetPresence will set presence for all IM channels -func (s *SlackService) SetPresenceChannels() { var wg sync.WaitGroup - for i, channel := range s.SlackChannels { + for _, chn := range slackChans { + chanItem := s.createChannelItem(chn) - switch channel := channel.(type) { - case slack.IM: - wg.Add(1) - go func(i int) { - presence, _ := s.GetUserPresence(channel.User) - s.Channels[i].Presence = presence - wg.Done() - }(i) + if chn.IsChannel { + if !chn.IsMember { + continue + } + + chanItem.Type = components.ChannelTypeChannel + + buckets[0][chn.ID] = &tempChan{ + channelItem: chanItem, + slackChannel: chn, + } } + if chn.IsGroup { + if !chn.IsMember { + continue + } + + // This is done because MpIM channels are also considered groups + if chn.IsMpIM { + if !chn.IsOpen { + continue + } + + chanItem.Type = components.ChannelTypeMpIM + + buckets[2][chn.ID] = &tempChan{ + channelItem: chanItem, + slackChannel: chn, + } + } else { + + chanItem.Type = components.ChannelTypeGroup + + buckets[1][chn.ID] = &tempChan{ + channelItem: chanItem, + slackChannel: chn, + } + } + } + + if chn.IsIM { + // Check if user is deleted, we do this by checking the user id, + // and see if we have the user in the UserCache + name, ok := s.UserCache[chn.User] + if !ok { + continue + } + + chanItem.Name = name + chanItem.Type = components.ChannelTypeIM + + buckets[3][chn.User] = &tempChan{ + channelItem: chanItem, + slackChannel: chn, + } + + wg.Add(1) + go func(user string, buckets map[int]map[string]*tempChan) { + defer wg.Done() + + presence, err := s.GetUserPresence(user) + if err != nil { + buckets[3][user].channelItem.Presence = "away" + return + } + + buckets[3][user].channelItem.Presence = presence + }(chn.User, buckets) + } } wg.Wait() -} -// SetPresenceChannelEvent will set the presence of a IM channel -func (s *SlackService) SetPresenceChannelEvent(userID string, presence string) { - // Get the correct Channel from svc.Channels - var index int - for i, channel := range s.Channels { - if userID == channel.UserID { - index = i - break + // Sort the buckets + var keys []int + for k := range buckets { + keys = append(keys, k) + } + sort.Ints(keys) + + var chans []components.ChannelItem + for _, k := range keys { + + bucket := buckets[k] + + // Sort channels in every bucket + tcArr := make([]tempChan, 0) + for _, v := range bucket { + tcArr = append(tcArr, *v) + } + + sort.Slice(tcArr, func(i, j int) bool { + return tcArr[i].channelItem.Name < tcArr[j].channelItem.Name + }) + + // Add ChannelItem and SlackChannel to the SlackService struct + for _, tc := range tcArr { + chans = append(chans, tc.channelItem) + s.Conversations = append(s.Conversations, tc.slackChannel) } } - s.Channels[index].Presence = presence -} -// GetSlackChannel returns the representation of a slack channel -func (s *SlackService) GetSlackChannel(selectedChannel int) interface{} { - return s.SlackChannels[selectedChannel] + return chans, nil } // GetUserPresence will get the presence of a specific user @@ -257,122 +248,47 @@ func (s *SlackService) GetUserPresence(userID string) (string, error) { return presence.Presence, nil } -// SetChannelReadMark will set the read mark for a channel, group, and im -// channel based on the current time. -func (s *SlackService) SetChannelReadMark(channel interface{}) { - switch channel := channel.(type) { - case slack.Channel: - s.Client.SetChannelReadMark( - channel.ID, fmt.Sprintf("%f", - float64(time.Now().Unix())), - ) - case slack.Group: - s.Client.SetGroupReadMark( - channel.ID, fmt.Sprintf("%f", - float64(time.Now().Unix())), - ) - case slack.IM: - s.Client.MarkIMChannel( - channel.ID, fmt.Sprintf("%f", - float64(time.Now().Unix())), - ) - } -} - // MarkAsRead will set the channel as read -func (s *SlackService) MarkAsRead(channelID int) { - channel := s.Channels[channelID] - - if channel.Notification { - s.Channels[channelID].Notification = false - - switch channel.Type { - case ChannelTypeChannel: - s.Client.SetChannelReadMark( - channel.ID, fmt.Sprintf("%f", - float64(time.Now().Unix())), - ) - case ChannelTypeGroup: - s.Client.SetGroupReadMark( - channel.ID, fmt.Sprintf("%f", - float64(time.Now().Unix())), - ) - case ChannelTypeIM: - s.Client.MarkIMChannel( - channel.ID, fmt.Sprintf("%f", - float64(time.Now().Unix())), - ) - } - } -} - -// FindChannel will loop over s.Channels to find the index where the -// channelID equals the ID -func (s *SlackService) FindChannel(channelID string) int { - var index int - for i, channel := range s.Channels { - if channel.ID == channelID { - index = i - break - } - } - return index -} - -// MarkAsUnread will set the channel as unread -func (s *SlackService) MarkAsUnread(channelID string) { - index := s.FindChannel(channelID) - s.Channels[index].Notification = true -} - -// GetChannelName will return the name for a specific channelID -func (s *SlackService) GetChannelName(channelID string) string { - index := s.FindChannel(channelID) - return s.Channels[index].Name +func (s *SlackService) MarkAsRead(channelID string) { + s.Client.SetChannelReadMark( + channelID, fmt.Sprintf("%f", + float64(time.Now().Unix())), + ) } // SendMessage will send a message to a particular channel -func (s *SlackService) SendMessage(channelID int, message string) { +func (s *SlackService) SendMessage(channelID string, message string) error { // https://godoc.org/github.com/nlopes/slack#PostMessageParameters postParams := slack.PostMessageParameters{ - AsUser: true, - Username: s.CurrentUsername, + AsUser: true, + Username: s.CurrentUsername, + LinkNames: 1, } // https://godoc.org/github.com/nlopes/slack#Client.PostMessage - s.Client.PostMessage(s.Channels[channelID].ID, message, postParams) + _, _, err := s.Client.PostMessage(channelID, message, postParams) + if err != nil { + return err + } + + return nil } // GetMessages will get messages for a channel, group or im channel delimited // by a count. -func (s *SlackService) GetMessages(channel interface{}, count int) []components.Message { - // https://api.slack.com/methods/channels.history - historyParams := slack.HistoryParameters{ - Count: count, +func (s *SlackService) GetMessages(channelID string, count int) ([]components.Message, error) { + + // https://godoc.org/github.com/nlopes/slack#GetConversationHistoryParameters + historyParams := slack.GetConversationHistoryParameters{ + ChannelID: channelID, + Limit: count, Inclusive: false, - Unreads: false, } - // https://godoc.org/github.com/nlopes/slack#History - history := new(slack.History) - var err error - switch chnType := channel.(type) { - case slack.Channel: - history, err = s.Client.GetChannelHistory(chnType.ID, historyParams) - if err != nil { - log.Fatal(err) // FIXME - } - case slack.Group: - history, err = s.Client.GetGroupHistory(chnType.ID, historyParams) - if err != nil { - log.Fatal(err) // FIXME - } - case slack.IM: - history, err = s.Client.GetIMHistory(chnType.ID, historyParams) - if err != nil { - log.Fatal(err) // FIXME - } + history, err := s.Client.GetConversationHistory(&historyParams) + if err != nil { + return nil, err } // Construct the messages @@ -389,7 +305,7 @@ func (s *SlackService) GetMessages(channel interface{}, count int) []components. messagesReversed = append(messagesReversed, messages[i]) } - return messagesReversed + return messagesReversed, nil } // CreateMessage will create a string formatted message that can be rendered @@ -447,12 +363,13 @@ func (s *SlackService) CreateMessage(message slack.Message) []components.Message // Format message msg := components.Message{ - Time: time.Unix(intTime, 0), - Name: name, - Content: parseMessage(s, message.Text), - StyleTime: s.Config.Theme.Message.Time, - StyleName: s.Config.Theme.Message.Name, - StyleText: s.Config.Theme.Message.Text, + Time: time.Unix(intTime, 0), + Name: name, + Content: parseMessage(s, message.Text), + StyleTime: s.Config.Theme.Message.Time, + StyleName: s.Config.Theme.Message.Name, + StyleText: s.Config.Theme.Message.Text, + FormatTime: s.Config.Theme.Message.TimeFormat, } msgs = append(msgs, msg) @@ -519,12 +436,13 @@ func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent // Format message msg := components.Message{ - Time: time.Unix(intTime, 0), - Name: name, - Content: parseMessage(s, message.Text), - StyleTime: s.Config.Theme.Message.Time, - StyleName: s.Config.Theme.Message.Name, - StyleText: s.Config.Theme.Message.Text, + Time: time.Unix(intTime, 0), + Name: name, + Content: parseMessage(s, message.Text), + StyleTime: s.Config.Theme.Message.Time, + StyleName: s.Config.Theme.Message.Name, + StyleText: s.Config.Theme.Message.Text, + FormatTime: s.Config.Theme.Message.TimeFormat, } msgs = append(msgs, msg) @@ -532,52 +450,13 @@ func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent return msgs, nil } -// CheckNotifyMention check if the message event is either contains a -// mention or is posted on an IM channel -func (s *SlackService) CheckNotifyMention(ev *slack.MessageEvent) bool { - channel := s.Channels[s.FindChannel(ev.Channel)] - switch channel.Type { - case ChannelTypeIM: - return true - } - - // Mentions have the following format: - // <@U12345|erroneousboat> - // <@U12345> - r := regexp.MustCompile(`\<@(\w+\|*\w+)\>`) - matches := r.FindAllString(ev.Text, -1) - for _, match := range matches { - if strings.Contains(match, s.CurrentUserID) { - return true - } - } - - return false -} - -func (s *SlackService) CreateNotifyMessage(channelID string) string { - channel := s.Channels[s.FindChannel(channelID)] - - switch channel.Type { - case ChannelTypeChannel: - return fmt.Sprintf("Message received on channel: %s", channel.Name) - case ChannelTypeGroup: - return fmt.Sprintf("Message received in group: %s", channel.Name) - case ChannelTypeIM: - return fmt.Sprintf("Message received from: %s", channel.Name) - } - - return "" -} - // parseMessage will parse a message string and find and replace: // - emoji's // - mentions func parseMessage(s *SlackService, msg string) string { - // NOTE: Commented out because rendering of the emoji's - // creates artifacts from the last view because of - // double width emoji's - // msg = parseEmoji(msg) + if s.Config.Emoji { + msg = parseEmoji(msg) + } msg = parseMentions(s, msg) @@ -657,9 +536,10 @@ func (s *SlackService) CreateMessageFromAttachments(atts []slack.Attachment) []c att.Fields[i].Title, att.Fields[i].Value, ), - StyleTime: s.Config.Theme.Message.Time, - StyleName: s.Config.Theme.Message.Name, - StyleText: s.Config.Theme.Message.Text, + StyleTime: s.Config.Theme.Message.Time, + StyleName: s.Config.Theme.Message.Name, + StyleText: s.Config.Theme.Message.Text, + FormatTime: s.Config.Theme.Message.TimeFormat, }, ) } @@ -667,17 +547,41 @@ func (s *SlackService) CreateMessageFromAttachments(atts []slack.Attachment) []c if att.Text != "" { msgs = append( msgs, - components.Message{Content: fmt.Sprintf("%s", att.Text)}, + components.Message{ + Content: fmt.Sprintf("%s", att.Text), + StyleTime: s.Config.Theme.Message.Time, + StyleName: s.Config.Theme.Message.Name, + StyleText: s.Config.Theme.Message.Text, + FormatTime: s.Config.Theme.Message.TimeFormat, + }, ) } if att.Title != "" { msgs = append( msgs, - components.Message{Content: fmt.Sprintf("%s", att.Title)}, + components.Message{ + Content: fmt.Sprintf("%s", att.Title), + StyleTime: s.Config.Theme.Message.Time, + StyleName: s.Config.Theme.Message.Name, + StyleText: s.Config.Theme.Message.Text, + FormatTime: s.Config.Theme.Message.TimeFormat, + }, ) } } return msgs } + +func (s *SlackService) createChannelItem(chn slack.Channel) components.ChannelItem { + return components.ChannelItem{ + ID: chn.ID, + Name: chn.Name, + Topic: chn.Topic.Value, + UserID: chn.User, + StylePrefix: s.Config.Theme.Channel.Prefix, + StyleIcon: s.Config.Theme.Channel.Icon, + StyleText: s.Config.Theme.Channel.Text, + } +} diff --git a/vendor/github.com/gorilla/websocket/.gitignore b/vendor/github.com/gorilla/websocket/.gitignore index ac71020..cd3fcd1 100644 --- a/vendor/github.com/gorilla/websocket/.gitignore +++ b/vendor/github.com/gorilla/websocket/.gitignore @@ -22,4 +22,4 @@ _testmain.go *.exe .idea/ -*.iml \ No newline at end of file +*.iml diff --git a/vendor/github.com/gorilla/websocket/.travis.yml b/vendor/github.com/gorilla/websocket/.travis.yml index 3d8d29c..1f73047 100644 --- a/vendor/github.com/gorilla/websocket/.travis.yml +++ b/vendor/github.com/gorilla/websocket/.travis.yml @@ -4,10 +4,12 @@ sudo: false matrix: include: - go: 1.4 - - go: 1.5 - - go: 1.6 - - go: 1.7 - - go: 1.8 + - go: 1.5.x + - go: 1.6.x + - go: 1.7.x + - go: 1.8.x + - go: 1.9.x + - go: 1.10.x - go: tip allow_failures: - go: tip diff --git a/vendor/github.com/gorilla/websocket/AUTHORS b/vendor/github.com/gorilla/websocket/AUTHORS index b003eca..1931f40 100644 --- a/vendor/github.com/gorilla/websocket/AUTHORS +++ b/vendor/github.com/gorilla/websocket/AUTHORS @@ -4,5 +4,6 @@ # Please keep the list sorted. Gary Burd +Google LLC (https://opensource.google.com/) Joachim Bauch diff --git a/vendor/github.com/gorilla/websocket/README.md b/vendor/github.com/gorilla/websocket/README.md index 33c3d2b..20e391f 100644 --- a/vendor/github.com/gorilla/websocket/README.md +++ b/vendor/github.com/gorilla/websocket/README.md @@ -51,7 +51,7 @@ subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn Write message using io.WriteCloserYesNo, see note 3 -Notes: +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 diff --git a/vendor/github.com/gorilla/websocket/client.go b/vendor/github.com/gorilla/websocket/client.go index 43a87c7..41f8ed5 100644 --- a/vendor/github.com/gorilla/websocket/client.go +++ b/vendor/github.com/gorilla/websocket/client.go @@ -5,10 +5,8 @@ package websocket import ( - "bufio" "bytes" "crypto/tls" - "encoding/base64" "errors" "io" "io/ioutil" @@ -88,50 +86,6 @@ type Dialer struct { var errMalformedURL = errors.New("malformed ws or wss URL") -// parseURL parses the URL. -// -// This function is a replacement for the standard library url.Parse function. -// In Go 1.4 and earlier, url.Parse loses information from the path. -func parseURL(s string) (*url.URL, error) { - // From the RFC: - // - // ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ] - // wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ] - var u url.URL - switch { - case strings.HasPrefix(s, "ws://"): - u.Scheme = "ws" - s = s[len("ws://"):] - case strings.HasPrefix(s, "wss://"): - u.Scheme = "wss" - s = s[len("wss://"):] - default: - return nil, errMalformedURL - } - - if i := strings.Index(s, "?"); i >= 0 { - u.RawQuery = s[i+1:] - s = s[:i] - } - - if i := strings.Index(s, "/"); i >= 0 { - u.Opaque = s[i:] - s = s[:i] - } else { - u.Opaque = "/" - } - - u.Host = s - - if strings.Contains(u.Host, "@") { - // Don't bother parsing user information because user information is - // not allowed in websocket URIs. - return nil, errMalformedURL - } - - return &u, nil -} - func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { hostPort = u.Host hostNoPort = u.Host @@ -150,11 +104,15 @@ func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { return hostPort, hostNoPort } -// DefaultDialer is a dialer with all fields set to the default zero values. +// DefaultDialer is a dialer with all fields set to the default values. var DefaultDialer = &Dialer{ - Proxy: http.ProxyFromEnvironment, + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: 45 * time.Second, } +// nilDialer is dialer to use when receiver is nil. +var nilDialer Dialer = *DefaultDialer + // Dial creates a new client connection. Use requestHeader to specify the // origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie). // Use the response.Header to get the selected subprotocol @@ -167,9 +125,7 @@ var DefaultDialer = &Dialer{ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { if d == nil { - d = &Dialer{ - Proxy: http.ProxyFromEnvironment, - } + d = &nilDialer } challengeKey, err := generateChallengeKey() @@ -177,7 +133,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re return nil, nil, err } - u, err := parseURL(urlStr) + u, err := url.Parse(urlStr) if err != nil { return nil, nil, err } @@ -237,31 +193,15 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re 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.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover") - } - - hostPort, hostNoPort := hostPortNoPort(u) - - var proxyURL *url.URL - // Check wether the proxy method has been configured - if d.Proxy != nil { - proxyURL, err = d.Proxy(req) - } - if err != nil { - return nil, nil, err - } - - var targetHostPort string - if proxyURL != nil { - targetHostPort, _ = hostPortNoPort(proxyURL) - } else { - targetHostPort = hostPort + req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"} } var deadline time.Time @@ -269,13 +209,47 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re deadline = time.Now().Add(d.HandshakeTimeout) } + // Get network dial function. netDial := d.NetDial if netDial == nil { netDialer := &net.Dialer{Deadline: deadline} netDial = netDialer.Dial } - netConn, err := netDial("tcp", targetHostPort) + // If needed, wrap the dial function to set the connection deadline. + if !deadline.Equal(time.Time{}) { + 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) + netConn, err := netDial("tcp", hostPort) if err != nil { return nil, nil, err } @@ -286,42 +260,6 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re } }() - if err := netConn.SetDeadline(deadline); err != nil { - return nil, nil, err - } - - if proxyURL != nil { - connectHeader := make(http.Header) - if user := proxyURL.User; user != nil { - proxyUser := user.Username() - if proxyPassword, passwordSet := user.Password(); passwordSet { - credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword)) - connectHeader.Set("Proxy-Authorization", "Basic "+credential) - } - } - connectReq := &http.Request{ - Method: "CONNECT", - URL: &url.URL{Opaque: hostPort}, - Host: hostPort, - Header: connectHeader, - } - - connectReq.Write(netConn) - - // Read response. - // Okay to use and discard buffered reader here, because - // TLS server will not speak until spoken to. - br := bufio.NewReader(netConn) - resp, err := http.ReadResponse(br, connectReq) - if err != nil { - return nil, nil, err - } - if resp.StatusCode != 200 { - f := strings.SplitN(resp.Status, " ", 2) - return nil, nil, errors.New(f[1]) - } - } - if u.Scheme == "https" { cfg := cloneTLSConfig(d.TLSClientConfig) if cfg.ServerName == "" { diff --git a/vendor/github.com/gorilla/websocket/conn.go b/vendor/github.com/gorilla/websocket/conn.go index 97e1dba..5f46bf4 100644 --- a/vendor/github.com/gorilla/websocket/conn.go +++ b/vendor/github.com/gorilla/websocket/conn.go @@ -76,7 +76,7 @@ const ( // is UTF-8 encoded text. PingMessage = 9 - // PongMessage denotes a ping control message. The optional message payload + // PongMessage denotes a pong control message. The optional message payload // is UTF-8 encoded text. PongMessage = 10 ) @@ -100,9 +100,8 @@ func (e *netError) Error() string { return e.msg } func (e *netError) Temporary() bool { return e.temporary } func (e *netError) Timeout() bool { return e.timeout } -// CloseError represents close frame. +// CloseError represents a close message. type CloseError struct { - // Code is defined in RFC 6455, section 11.7. Code int @@ -343,7 +342,8 @@ func (c *Conn) Subprotocol() string { return c.subprotocol } -// Close closes the underlying network connection without sending or waiting for a close frame. +// Close closes the underlying network connection without sending or waiting +// for a close message. func (c *Conn) Close() error { return c.conn.Close() } @@ -370,7 +370,7 @@ func (c *Conn) writeFatal(err error) error { return err } -func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error { +func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error { <-c.mu defer func() { c.mu <- true }() @@ -382,15 +382,14 @@ func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error { } c.conn.SetWriteDeadline(deadline) - for _, buf := range bufs { - if len(buf) > 0 { - _, err := c.conn.Write(buf) - if err != nil { - return c.writeFatal(err) - } - } + if 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) } @@ -484,6 +483,9 @@ func (c *Conn) prepWrite(messageType int) error { // // 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 @@ -764,7 +766,6 @@ func (c *Conn) SetWriteDeadline(t time.Time) error { // Read methods func (c *Conn) advanceFrame() (int, error) { - // 1. Skip remainder of previous frame. if c.readRemaining > 0 { @@ -1033,7 +1034,7 @@ func (c *Conn) SetReadDeadline(t time.Time) error { } // SetReadLimit sets the maximum size for a message read from the peer. If a -// message exceeds the limit, the connection sends a close frame to the peer +// 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 @@ -1046,24 +1047,22 @@ func (c *Conn) CloseHandler() func(code int, text string) error { // SetCloseHandler sets the handler for close messages received from the peer. // The code argument to h is the received close code or CloseNoStatusReceived -// if the close message is empty. The default close handler sends a close frame -// back to the peer. +// if the close message is empty. The default close handler sends a close +// message back to the peer. // -// The application must read the connection to process close messages as -// described in the section on Control Frames above. +// 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 frame is +// 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 frame back to +// 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 := []byte{} - if code != CloseNoStatusReceived { - message = FormatCloseMessage(code, "") - } + message := FormatCloseMessage(code, "") c.WriteControl(CloseMessage, message, time.Now().Add(writeWait)) return nil } @@ -1077,11 +1076,12 @@ func (c *Conn) PingHandler() func(appData string) error { } // SetPingHandler sets the handler for ping messages received from the peer. -// The appData argument to h is the PING frame application data. The default +// The appData argument to h is the PING message application data. The default // ping handler sends a pong to the peer. // -// The application must read the connection to process ping messages as -// described in the section on Control Frames above. +// 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 { @@ -1103,11 +1103,12 @@ func (c *Conn) PongHandler() func(appData string) error { } // SetPongHandler sets the handler for pong messages received from the peer. -// The appData argument to h is the PONG frame application data. The default +// The appData argument to h is the PONG message application data. The default // pong handler does nothing. // -// The application must read the connection to process ping messages as -// described in the section on Control Frames above. +// 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 } @@ -1141,7 +1142,14 @@ func (c *Conn) SetCompressionLevel(level int) error { } // 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) 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 index e291a95..dcce1a6 100644 --- a/vendor/github.com/gorilla/websocket/doc.go +++ b/vendor/github.com/gorilla/websocket/doc.go @@ -6,9 +6,8 @@ // // Overview // -// The Conn type represents a WebSocket connection. A server application uses -// the Upgrade function from an Upgrader object with a HTTP request handler -// to get a pointer to a Conn: +// 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, @@ -31,10 +30,12 @@ // for { // messageType, p, err := conn.ReadMessage() // if err != nil { +// log.Println(err) // return // } -// if err = conn.WriteMessage(messageType, p); err != nil { -// return err +// if err := conn.WriteMessage(messageType, p); err != nil { +// log.Println(err) +// return // } // } // @@ -85,20 +86,26 @@ // and pong. Call the connection WriteControl, WriteMessage or NextWriter // methods to send a control message to the peer. // -// Connections handle received close messages by sending a close message to the -// peer and returning a *CloseError from the the NextReader, ReadMessage or the -// message Read method. +// Connections handle received 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 and pong messages by invoking callback -// functions set with SetPingHandler and SetPongHandler methods. The callback -// functions are called from the NextReader, ReadMessage and the message Read -// methods. +// 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. // -// The default ping handler sends a pong to the peer. The application's reading -// goroutine can block for a short time while the handler writes the pong data -// to the connection. +// 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 application must read the connection to process ping, pong and close +// 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: @@ -137,19 +144,12 @@ // method fails the WebSocket handshake with HTTP status 403. // // If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail -// the handshake if the Origin request header is present and not equal to the -// Host request header. +// the handshake if the Origin request header is present and the Origin host is +// not equal to the Host request header. // -// An application can allow connections from any origin by specifying a -// function that always returns true: -// -// var upgrader = websocket.Upgrader{ -// CheckOrigin: func(r *http.Request) bool { return true }, -// } -// -// The deprecated Upgrade function does not enforce an origin policy. It's the -// application's responsibility to check the Origin header before calling -// Upgrade. +// 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 // diff --git a/vendor/github.com/gorilla/websocket/json.go b/vendor/github.com/gorilla/websocket/json.go index 4f0e368..dc2c1f6 100644 --- a/vendor/github.com/gorilla/websocket/json.go +++ b/vendor/github.com/gorilla/websocket/json.go @@ -9,12 +9,14 @@ import ( "io" ) -// WriteJSON is deprecated, use c.WriteJSON instead. +// 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 to the connection. +// 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. @@ -31,7 +33,10 @@ func (c *Conn) WriteJSON(v interface{}) error { return err2 } -// ReadJSON is deprecated, use c.ReadJSON instead. +// 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) } diff --git a/vendor/github.com/gorilla/websocket/mask.go b/vendor/github.com/gorilla/websocket/mask.go index 6a88bbc..577fce9 100644 --- a/vendor/github.com/gorilla/websocket/mask.go +++ b/vendor/github.com/gorilla/websocket/mask.go @@ -11,7 +11,6 @@ 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 { 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 index 3495e0f..4834c38 100644 --- a/vendor/github.com/gorilla/websocket/server.go +++ b/vendor/github.com/gorilla/websocket/server.go @@ -34,9 +34,11 @@ type Upgrader struct { ReadBufferSize, WriteBufferSize int // Subprotocols specifies the server's supported protocols in order of - // preference. If this field is set, then the Upgrade method negotiates a + // 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. + // 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 @@ -44,8 +46,12 @@ type Upgrader struct { Error func(w http.ResponseWriter, r *http.Request, status int, reason error) // CheckOrigin returns true if the request Origin header is acceptable. If - // CheckOrigin is nil, the host in the Origin header must not be set or - // must match the host of the request. + // CheckOrigin 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 @@ -76,7 +82,7 @@ func checkSameOrigin(r *http.Request) bool { if err != nil { return false } - return u.Host == r.Host + return equalASCIIFold(u.Host, r.Host) } func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string { @@ -99,42 +105,44 @@ func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header // // 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). +// application negotiated subprotocol (Sec-WebSocket-Protocol). // // If the upgrade fails, then Upgrade replies to the client with an HTTP error // response. func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) { - if r.Method != "GET" { - return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: not a websocket handshake: request method is not GET") - } - - if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok { - return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-Websocket-Extensions' headers are unsupported") - } + const badHandshake = "websocket: the client is not using the websocket protocol: " if !tokenListContainsValue(r.Header, "Connection", "upgrade") { - return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'upgrade' token not found in 'Connection' header") + 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, "websocket: not a websocket handshake: 'websocket' token not found in 'Upgrade' header") + 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: 'Origin' header value not allowed") + 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") + 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) @@ -184,12 +192,12 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade p = append(p, computeAcceptKey(challengeKey)...) p = append(p, "\r\n"...) if c.subprotocol != "" { - p = append(p, "Sec-Websocket-Protocol: "...) + 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"...) + 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" { @@ -230,13 +238,14 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade // Upgrade upgrades the HTTP server connection to the WebSocket protocol. // -// This function is deprecated, use websocket.Upgrader instead. +// Deprecated: Use websocket.Upgrader instead. // -// The application is responsible for checking the request origin before -// calling Upgrade. An example implementation of the same origin policy is: +// 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", 403) +// http.Error(w, "Origin not allowed", http.StatusForbidden) // return // } // diff --git a/vendor/github.com/gorilla/websocket/util.go b/vendor/github.com/gorilla/websocket/util.go index 9a4908d..385fa01 100644 --- a/vendor/github.com/gorilla/websocket/util.go +++ b/vendor/github.com/gorilla/websocket/util.go @@ -11,6 +11,7 @@ import ( "io" "net/http" "strings" + "unicode/utf8" ) var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") @@ -111,14 +112,14 @@ func nextTokenOrQuoted(s string) (value string, rest string) { case escape: escape = false p[j] = b - j += 1 + j++ case b == '\\': escape = true case b == '"': return string(p[:j]), s[i+1:] default: p[j] = b - j += 1 + j++ } } return "", "" @@ -127,8 +128,31 @@ func nextTokenOrQuoted(s string) (value string, rest string) { 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 token. +// 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] { @@ -142,7 +166,7 @@ headers: if s != "" && s[0] != ',' { continue headers } - if strings.EqualFold(t, value) { + if equalASCIIFold(t, value) { return true } if s == "" { @@ -156,7 +180,6 @@ headers: // parseExtensiosn parses WebSocket extensions from a header. func parseExtensions(header http.Header) []map[string]string { - // From RFC 6455: // // Sec-WebSocket-Extensions = extension-list 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/maruel/panicparse/stack/bucket.go b/vendor/github.com/maruel/panicparse/stack/bucket.go new file mode 100644 index 0000000..259dab2 --- /dev/null +++ b/vendor/github.com/maruel/panicparse/stack/bucket.go @@ -0,0 +1,109 @@ +// Copyright 2015 Marc-Antoine Ruel. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package stack + +import ( + "sort" +) + +// Similarity is the level at which two call lines arguments must match to be +// considered similar enough to coalesce them. +type Similarity int + +const ( + // ExactFlags requires same bits (e.g. Locked). + ExactFlags Similarity = iota + // ExactLines requests the exact same arguments on the call line. + ExactLines + // AnyPointer considers different pointers a similar call line. + AnyPointer + // AnyValue accepts any value as similar call line. + AnyValue +) + +// Bucketize returns the number of similar goroutines. +func Bucketize(goroutines []Goroutine, similar Similarity) map[*Signature][]Goroutine { + out := map[*Signature][]Goroutine{} + // O(n²). Fix eventually. + for _, routine := range goroutines { + found := false + for key := range out { + // When a match is found, this effectively drops the other goroutine ID. + if key.Similar(&routine.Signature, similar) { + found = true + if !key.Equal(&routine.Signature) { + // Almost but not quite equal. There's different pointers passed + // around but the same values. Zap out the different values. + newKey := key.Merge(&routine.Signature) + out[newKey] = append(out[key], routine) + delete(out, key) + } else { + out[key] = append(out[key], routine) + } + break + } + } + if !found { + key := &Signature{} + *key = routine.Signature + out[key] = []Goroutine{routine} + } + } + return out +} + +// Bucket is a stack trace signature and the list of goroutines that fits this +// signature. +type Bucket struct { + Signature + Routines []Goroutine +} + +// First returns true if it contains the first goroutine, e.g. the ones that +// likely generated the panic() call, if any. +func (b *Bucket) First() bool { + for _, r := range b.Routines { + if r.First { + return true + } + } + return false +} + +// Less does reverse sort. +func (b *Bucket) Less(r *Bucket) bool { + if b.First() { + return true + } + if r.First() { + return false + } + return b.Signature.Less(&r.Signature) +} + +// Buckets is a list of Bucket sorted by repeation count. +type Buckets []Bucket + +func (b Buckets) Len() int { + return len(b) +} + +func (b Buckets) Less(i, j int) bool { + return b[i].Less(&b[j]) +} + +func (b Buckets) Swap(i, j int) { + b[j], b[i] = b[i], b[j] +} + +// SortBuckets creates a list of Bucket from each goroutine stack trace count. +func SortBuckets(buckets map[*Signature][]Goroutine) Buckets { + out := make(Buckets, 0, len(buckets)) + for signature, count := range buckets { + out = append(out, Bucket{*signature, count}) + } + sort.Sort(out) + return out +} diff --git a/vendor/github.com/maruel/panicparse/stack/source.go b/vendor/github.com/maruel/panicparse/stack/source.go index f09e673..f5fb8fb 100644 --- a/vendor/github.com/maruel/panicparse/stack/source.go +++ b/vendor/github.com/maruel/panicparse/stack/source.go @@ -49,11 +49,11 @@ func (c *cache) augmentGoroutine(goroutine *Goroutine) { // For each call site, look at the next call and populate it. Then we can // walk back and reformat things. for i := range goroutine.Stack.Calls { - c.load(goroutine.Stack.Calls[i].SourcePath) + c.load(goroutine.Stack.Calls[i].LocalSourcePath()) } // Once all loaded, we can look at the next call when available. - for i := 1; i < len(goroutine.Stack.Calls); i++ { + for i := 0; i < len(goroutine.Stack.Calls)-1; i++ { // Get the AST from the previous call and process the call line with it. if f := c.getFuncAST(&goroutine.Stack.Calls[i]); f != nil { processCall(&goroutine.Stack.Calls[i], f) @@ -101,7 +101,7 @@ func (c *cache) load(fileName string) { } func (c *cache) getFuncAST(call *Call) *ast.FuncDecl { - if p := c.parsed[call.SourcePath]; p != nil { + if p := c.parsed[call.LocalSourcePath()]; p != nil { return p.getFuncAST(call.Func.Name(), call.Line) } return nil @@ -115,6 +115,15 @@ type parsedFile struct { // getFuncAST gets the callee site function AST representation for the code // inside the function f at line l. func (p *parsedFile) getFuncAST(f string, l int) (d *ast.FuncDecl) { + if len(p.lineToByteOffset) <= l { + // The line number in the stack trace line does not exist in the file. That + // can only mean that the sources on disk do not match the sources used to + // build the binary. + // TODO(maruel): This should be surfaced, so that source parsing is + // completely ignored. + return + } + // Walk the AST to find the lineToByteOffset that fits the line number. var lastFunc *ast.FuncDecl var found ast.Node @@ -155,20 +164,18 @@ func (p *parsedFile) getFuncAST(f string, l int) (d *ast.FuncDecl) { } func name(n ast.Node) string { - if _, ok := n.(*ast.InterfaceType); ok { + switch t := n.(type) { + case *ast.InterfaceType: return "interface{}" + case *ast.Ident: + return t.Name + case *ast.SelectorExpr: + return t.Sel.Name + case *ast.StarExpr: + return "*" + name(t.X) + default: + return "" } - if i, ok := n.(*ast.Ident); ok { - return i.Name - } - if _, ok := n.(*ast.FuncType); ok { - return "func" - } - if s, ok := n.(*ast.SelectorExpr); ok { - return s.Sel.Name - } - // TODO(maruel): Implement anything missing. - return "" } // fieldToType returns the type name and whether if it's an ellipsis. @@ -189,6 +196,10 @@ func fieldToType(f *ast.Field) (string, bool) { return arg.Sel.Name, false case *ast.StarExpr: return "*" + name(arg.X), false + case *ast.MapType: + return fmt.Sprintf("map[%s]%s", name(arg.Key), name(arg.Value)), false + case *ast.ChanType: + return fmt.Sprintf("chan %s", name(arg.Value)), false default: // TODO(maruel): Implement anything missing. return "", false diff --git a/vendor/github.com/maruel/panicparse/stack/stack.go b/vendor/github.com/maruel/panicparse/stack/stack.go index cfb502e..0aad495 100644 --- a/vendor/github.com/maruel/panicparse/stack/stack.go +++ b/vendor/github.com/maruel/panicparse/stack/stack.go @@ -5,7 +5,7 @@ // Package stack analyzes stack dump of Go processes and simplifies it. // // It is mostly useful on servers will large number of identical goroutines, -// making the crash dump harder to read than strictly necesary. +// making the crash dump harder to read than strictly necessary. package stack import ( @@ -14,9 +14,11 @@ import ( "errors" "fmt" "io" + "log" "math" "net/url" "os" + "os/user" "path/filepath" "regexp" "runtime" @@ -35,7 +37,7 @@ var ( // - found next stack barrier at 0x123; expected // - runtime: unexpected return pc for FUNC_NAME called from 0x123 - reRoutineHeader = regexp.MustCompile("^goroutine (\\d+) \\[([^\\]]+)\\]\\:\n$") + reRoutineHeader = regexp.MustCompile("^goroutine (\\d+) \\[([^\\]]+)\\]\\:\r?\n$") reMinutes = regexp.MustCompile("^(\\d+) minutes$") reUnavail = regexp.MustCompile("^(?:\t| +)goroutine running on other thread; stack unavailable") // See gentraceback() in src/runtime/traceback.go for more information. @@ -54,34 +56,30 @@ var ( // when a signal is not correctly handled. It is printed with m.throwing>0. // These are discarded. // - For cgo, the source file may be "??". - reFile = regexp.MustCompile("^(?:\t| +)(\\?\\?|\\|.+\\.(?:c|go|s))\\:(\\d+)(?:| \\+0x[0-9a-f]+)(?:| fp=0x[0-9a-f]+ sp=0x[0-9a-f]+)\n$") + reFile = regexp.MustCompile("^(?:\t| +)(\\?\\?|\\|.+\\.(?:c|go|s))\\:(\\d+)(?:| \\+0x[0-9a-f]+)(?:| fp=0x[0-9a-f]+ sp=0x[0-9a-f]+)\r?\n$") // Sadly, it doesn't note the goroutine number so we could cascade them per // parenthood. - reCreated = regexp.MustCompile("^created by (.+)\n$") - reFunc = regexp.MustCompile("^(.+)\\((.*)\\)\n$") - reElided = regexp.MustCompile("^\\.\\.\\.additional frames elided\\.\\.\\.\n$") - // Include frequent GOROOT value on Windows, distro provided and user - // installed path. This simplifies the user's life when processing a trace - // generated on another VM. - // TODO(maruel): Guess the path automatically via traces containing the - // 'runtime' package, which is very frequent. This would be "less bad" than - // throwing up random values at the parser. - goroots = []string{runtime.GOROOT(), "c:/go", "/usr/lib/go", "/usr/local/go"} -) + reCreated = regexp.MustCompile("^created by (.+)\r?\n$") + reFunc = regexp.MustCompile("^(.+)\\((.*)\\)\r?\n$") + reElided = regexp.MustCompile("^\\.\\.\\.additional frames elided\\.\\.\\.\r?\n$") -// Similarity is the level at which two call lines arguments must match to be -// considered similar enough to coalesce them. -type Similarity int + // TODO(maruel): This is a global state, affected by ParseDump(). This will + // be refactored in v2. -const ( - // ExactFlags requires same bits (e.g. Locked). - ExactFlags Similarity = iota - // ExactLines requests the exact same arguments on the call line. - ExactLines - // AnyPointer considers different pointers a similar call line. - AnyPointer - // AnyValue accepts any value as similar call line. - AnyValue + // goroot is the GOROOT as detected in the traceback, not the on the host. + // + // It can be empty if no root was determined, for example the traceback + // contains only non-stdlib source references. + goroot string + // gopaths is the GOPATH as detected in the traceback, with the value being + // the corresponding path mapped to the host. + // + // It can be empty if only stdlib code is in the traceback or if no local + // sources were matched up. In the general case there is only one. + gopaths map[string]string + // Corresponding local values on the host. + localgoroot = runtime.GOROOT() + localgopaths = getGOPATHs() ) // Function is a function call. @@ -250,7 +248,7 @@ func (a *Args) Merge(r *Args) Args { // Call is an item in the stack trace. type Call struct { - SourcePath string // Full path name of the source file + SourcePath string // Full path name of the source file as seen in the trace Line int // Line number Func Function // Fully qualified function name (encoded). Args Args // Call arguments @@ -287,7 +285,23 @@ func (c *Call) SourceLine() string { return fmt.Sprintf("%s:%d", c.SourceName(), c.Line) } +// LocalSourcePath is the full path name of the source file as seen in the host. +func (c *Call) LocalSourcePath() string { + // TODO(maruel): Call needs members goroot and gopaths. + if strings.HasPrefix(c.SourcePath, goroot) { + return filepath.Join(localgoroot, c.SourcePath[len(goroot):]) + } + for prefix, dest := range gopaths { + if strings.HasPrefix(c.SourcePath, prefix) { + return filepath.Join(dest, c.SourcePath[len(prefix):]) + } + } + return c.SourcePath +} + // FullSourceLine returns "/path/to/source.go:line". +// +// This file path is mutated to look like the local path. func (c *Call) FullSourceLine() string { return fmt.Sprintf("%s:%d", c.SourcePath, c.Line) } @@ -302,13 +316,8 @@ const testMainSource = "_test" + string(os.PathSeparator) + "_testmain.go" // IsStdlib returns true if it is a Go standard library function. This includes // the 'go test' generated main executable. func (c *Call) IsStdlib() bool { - for _, goroot := range goroots { - if strings.HasPrefix(c.SourcePath, goroot) { - return true - } - } // Consider _test/_testmain.go as stdlib since it's injected by "go test". - return c.PkgSource() == testMainSource + return (goroot != "" && strings.HasPrefix(c.SourcePath, goroot)) || c.PkgSource() == testMainSource } // IsPkgMain returns true if it is in the main package. @@ -525,91 +534,6 @@ type Goroutine struct { First bool // First is the goroutine first printed, normally the one that crashed. } -// Bucketize returns the number of similar goroutines. -func Bucketize(goroutines []Goroutine, similar Similarity) map[*Signature][]Goroutine { - out := map[*Signature][]Goroutine{} - // O(n²). Fix eventually. - for _, routine := range goroutines { - found := false - for key := range out { - // When a match is found, this effectively drops the other goroutine ID. - if key.Similar(&routine.Signature, similar) { - found = true - if !key.Equal(&routine.Signature) { - // Almost but not quite equal. There's different pointers passed - // around but the same values. Zap out the different values. - newKey := key.Merge(&routine.Signature) - out[newKey] = append(out[key], routine) - delete(out, key) - } else { - out[key] = append(out[key], routine) - } - break - } - } - if !found { - key := &Signature{} - *key = routine.Signature - out[key] = []Goroutine{routine} - } - } - return out -} - -// Bucket is a stack trace signature and the list of goroutines that fits this -// signature. -type Bucket struct { - Signature - Routines []Goroutine -} - -// First returns true if it contains the first goroutine, e.g. the ones that -// likely generated the panic() call, if any. -func (b *Bucket) First() bool { - for _, r := range b.Routines { - if r.First { - return true - } - } - return false -} - -// Less does reverse sort. -func (b *Bucket) Less(r *Bucket) bool { - if b.First() { - return true - } - if r.First() { - return false - } - return b.Signature.Less(&r.Signature) -} - -// Buckets is a list of Bucket sorted by repeation count. -type Buckets []Bucket - -func (b Buckets) Len() int { - return len(b) -} - -func (b Buckets) Less(i, j int) bool { - return b[i].Less(&b[j]) -} - -func (b Buckets) Swap(i, j int) { - b[j], b[i] = b[i], b[j] -} - -// SortBuckets creates a list of Bucket from each goroutine stack trace count. -func SortBuckets(buckets map[*Signature][]Goroutine) Buckets { - out := make(Buckets, 0, len(buckets)) - for signature, count := range buckets { - out = append(out, Bucket{*signature, count}) - } - sort.Sort(out) - return out -} - // scanLines is similar to bufio.ScanLines except that it: // - doesn't drop '\n' // - doesn't strip '\r' @@ -656,7 +580,7 @@ func ParseDump(r io.Reader, out io.Writer) ([]Goroutine, error) { firstLine := false for scanner.Scan() { line := scanner.Text() - if line == "\n" { + if line == "\n" || line == "\r\n" { if goroutine != nil { goroutine = nil continue @@ -763,13 +687,30 @@ func ParseDump(r io.Reader, out io.Writer) ([]Goroutine, error) { goroutine = nil } nameArguments(goroutines) + // Mutate global state. + // TODO(maruel): Make this part of the context instead of a global. + if goroot == "" { + findRoots(goroutines) + } return goroutines, scanner.Err() } +// NoRebase disables GOROOT and GOPATH guessing in ParseDump(). +// +// BUG: This function will be removed in v2, as ParseDump() will accept a flag +// explicitly. +func NoRebase() { + goroot = runtime.GOROOT() + gopaths = map[string]string{} + for _, p := range getGOPATHs() { + gopaths[p] = p + } +} + // Private stuff. func nameArguments(goroutines []Goroutine) { - // Set a name for any pointer occuring more than once. + // Set a name for any pointer occurring more than once. type object struct { args []*Arg inPrimary bool @@ -791,7 +732,7 @@ func nameArguments(goroutines []Goroutine) { } // CreatedBy.Args is never set. } - order := uint64Slice{} + order := make(uint64Slice, 0, len(objects)/2) for k, obj := range objects { if len(obj.args) > 1 && obj.inPrimary { order = append(order, k) @@ -807,7 +748,7 @@ func nameArguments(goroutines []Goroutine) { } // Now do the rest. This is done so the output is deterministic. - order = uint64Slice{} + order = make(uint64Slice, 0, len(objects)) for k := range objects { order = append(order, k) } @@ -825,6 +766,139 @@ func nameArguments(goroutines []Goroutine) { } } +// hasPathPrefix returns true if any of s is the prefix of p. +func hasPathPrefix(p string, s map[string]string) bool { + for prefix := range s { + if strings.HasPrefix(p, prefix+"/") { + return true + } + } + return false +} + +// getFiles returns all the source files deduped and ordered. +func getFiles(goroutines []Goroutine) []string { + files := map[string]struct{}{} + for _, g := range goroutines { + for _, c := range g.Stack.Calls { + files[c.SourcePath] = struct{}{} + } + } + out := make([]string, 0, len(files)) + for f := range files { + out = append(out, f) + } + sort.Strings(out) + return out +} + +// splitPath splits a path into its components. +// +// The first item has its initial path separator kept. +func splitPath(p string) []string { + if p == "" { + return nil + } + var out []string + s := "" + for _, c := range p { + if c != '/' || (len(out) == 0 && strings.Count(s, "/") == len(s)) { + s += string(c) + } else if s != "" { + out = append(out, s) + s = "" + } + } + if s != "" { + out = append(out, s) + } + return out +} + +// isFile returns true if the path is a valid file. +func isFile(p string) bool { + // TODO(maruel): Is it faster to open the file or to stat it? Worth a perf + // test on Windows. + i, err := os.Stat(p) + return err == nil && !i.IsDir() +} + +// isRootIn returns a root if the file split in parts is rooted in root. +func rootedIn(root string, parts []string) string { + //log.Printf("rootIn(%s, %v)", root, parts) + for i := 1; i < len(parts); i++ { + suffix := filepath.Join(parts[i:]...) + if isFile(filepath.Join(root, suffix)) { + return filepath.Join(parts[:i]...) + } + } + return "" +} + +// findRoots sets global variables goroot and gopath. +// +// TODO(maruel): In v2, it will be a property of the new struct that will +// contain the goroutines. +func findRoots(goroutines []Goroutine) { + gopaths = map[string]string{} + for _, f := range getFiles(goroutines) { + // TODO(maruel): Could a stack dump have mixed cases? I think it's + // possible, need to confirm and handle. + //log.Printf(" Analyzing %s", f) + if goroot != "" && strings.HasPrefix(f, goroot+"/") { + continue + } + if gopaths != nil && hasPathPrefix(f, gopaths) { + continue + } + parts := splitPath(f) + if goroot == "" { + if r := rootedIn(localgoroot, parts); r != "" { + goroot = r + log.Printf("Found GOROOT=%s", goroot) + continue + } + } + found := false + for _, l := range localgopaths { + if r := rootedIn(l, parts); r != "" { + log.Printf("Found GOPATH=%s", r) + gopaths[r] = l + found = true + break + } + } + if !found { + // If the source is not found, just too bad. + //log.Printf("Failed to find locally: %s / %s", f, goroot) + } + } +} + +func getGOPATHs() []string { + var out []string + for _, v := range filepath.SplitList(os.Getenv("GOPATH")) { + // Disallow non-absolute paths? + if v != "" { + out = append(out, v) + } + } + if len(out) == 0 { + homeDir := "" + u, err := user.Current() + if err != nil { + homeDir = os.Getenv("HOME") + if homeDir == "" { + panic(fmt.Sprintf("Could not get current user or $HOME: %s\n", err.Error())) + } + } else { + homeDir = u.HomeDir + } + out = []string{homeDir + "go"} + } + return out +} + type uint64Slice []uint64 func (a uint64Slice) Len() int { return len(a) } diff --git a/vendor/github.com/mattn/go-runewidth/runewidth.go b/vendor/github.com/mattn/go-runewidth/runewidth.go index 2164497..82568a1 100644 --- a/vendor/github.com/mattn/go-runewidth/runewidth.go +++ b/vendor/github.com/mattn/go-runewidth/runewidth.go @@ -1,13 +1,24 @@ package runewidth +import "os" + var ( // EastAsianWidth will be set true if the current locale is CJK - EastAsianWidth = IsEastAsian() + EastAsianWidth bool // DefaultCondition is a condition in current locale DefaultCondition = &Condition{EastAsianWidth} ) +func init() { + env := os.Getenv("RUNEWIDTH_EASTASIAN") + if env == "" { + EastAsianWidth = IsEastAsian() + } else { + EastAsianWidth = env == "1" + } +} + type interval struct { first rune last rune @@ -55,6 +66,7 @@ var private = table{ var nonprint = table{ {0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD}, {0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F}, + {0x2028, 0x2029}, {0x202A, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF}, {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF}, } diff --git a/vendor/github.com/nlopes/slack/.gitignore b/vendor/github.com/nlopes/slack/.gitignore index dd2440d..ac6f3ee 100644 --- a/vendor/github.com/nlopes/slack/.gitignore +++ b/vendor/github.com/nlopes/slack/.gitignore @@ -1,2 +1,3 @@ *.test *~ +.idea/ diff --git a/vendor/github.com/nlopes/slack/CHANGELOG.md b/vendor/github.com/nlopes/slack/CHANGELOG.md index 8c4772d..a79ea50 100644 --- a/vendor/github.com/nlopes/slack/CHANGELOG.md +++ b/vendor/github.com/nlopes/slack/CHANGELOG.md @@ -1,3 +1,16 @@ +### 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. 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/README.md b/vendor/github.com/nlopes/slack/README.md index 953b9d8..849e8bd 100644 --- a/vendor/github.com/nlopes/slack/README.md +++ b/vendor/github.com/nlopes/slack/README.md @@ -7,19 +7,20 @@ 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.1.0 - May 28, 2017 +### v0.2.0 - Feb 10, 2018 -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. +Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against. -Please check [0.1.0](https://github.com/nlopes/slack/releases/tag/v0.1.0) +Please check [0.2.0](https://github.com/nlopes/slack/releases/tag/v0.2.0) ### CHANGELOG.md -As of this version a [CHANGELOG.md](https://github.com/nlopes/slack/blob/master/CHANGELOG.md) is available. Please visit it for updates. + [CHANGELOG.md](https://github.com/nlopes/slack/blob/master/CHANGELOG.md) is available. Please visit it for updates. ## Installing @@ -79,6 +80,11 @@ func main() { 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 diff --git a/vendor/github.com/nlopes/slack/admin.go b/vendor/github.com/nlopes/slack/admin.go index 4a7e0b1..a2aa7e5 100644 --- a/vendor/github.com/nlopes/slack/admin.go +++ b/vendor/github.com/nlopes/slack/admin.go @@ -62,6 +62,7 @@ func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, fi "last_name": {lastName}, "ultra_restricted": {"1"}, "token": {api.token}, + "resend": {"true"}, "set_active": {"true"}, "_attempts": {"1"}, } @@ -88,6 +89,7 @@ func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channe "last_name": {lastName}, "restricted": {"1"}, "token": {api.token}, + "resend": {"true"}, "set_active": {"true"}, "_attempts": {"1"}, } diff --git a/vendor/github.com/nlopes/slack/attachments.go b/vendor/github.com/nlopes/slack/attachments.go index d2b8b23..326fc01 100644 --- a/vendor/github.com/nlopes/slack/attachments.go +++ b/vendor/github.com/nlopes/slack/attachments.go @@ -78,6 +78,7 @@ type Attachment struct { 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"` diff --git a/vendor/github.com/nlopes/slack/backoff.go b/vendor/github.com/nlopes/slack/backoff.go index e555a1a..197bce2 100644 --- a/vendor/github.com/nlopes/slack/backoff.go +++ b/vendor/github.com/nlopes/slack/backoff.go @@ -38,7 +38,7 @@ func (b *backoff) Duration() time.Duration { } //calculate this duration dur := float64(b.Min) * math.Pow(b.Factor, float64(b.attempts)) - if b.Jitter == true { + if b.Jitter { dur = rand.Float64()*(dur-float64(b.Min)) + float64(b.Min) } //cap! diff --git a/vendor/github.com/nlopes/slack/bots.go b/vendor/github.com/nlopes/slack/bots.go index cb06088..92570a0 100644 --- a/vendor/github.com/nlopes/slack/bots.go +++ b/vendor/github.com/nlopes/slack/bots.go @@ -21,7 +21,7 @@ type botResponseFull struct { func botRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*botResponseFull, error) { response := &botResponseFull{} - err := post(ctx, client, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/channels.go b/vendor/github.com/nlopes/slack/channels.go index b16e19f..6204315 100644 --- a/vendor/github.com/nlopes/slack/channels.go +++ b/vendor/github.com/nlopes/slack/channels.go @@ -52,11 +52,8 @@ func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) "channel": {channelID}, } - if _, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug); err != nil { - return err - } - - return nil + _, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug) + return err } // UnarchiveChannel unarchives the given channel @@ -73,11 +70,8 @@ func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string "channel": {channelID}, } - if _, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug); err != nil { - return err - } - - return nil + _, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug) + return err } // CreateChannel creates a channel with the given name and returns a *Channel @@ -247,11 +241,8 @@ func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, us "user": {user}, } - if _, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug); err != nil { - return err - } - - return nil + _, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug) + return err } // GetChannels retrieves all the channels @@ -297,11 +288,8 @@ func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts "ts": {ts}, } - if _, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug); err != nil { - return err - } - - return nil + _, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug) + return err } // RenameChannel renames a given channel diff --git a/vendor/github.com/nlopes/slack/chat.go b/vendor/github.com/nlopes/slack/chat.go index fae416b..2b89a44 100644 --- a/vendor/github.com/nlopes/slack/chat.go +++ b/vendor/github.com/nlopes/slack/chat.go @@ -3,7 +3,6 @@ package slack import ( "context" "encoding/json" - "errors" "net/url" "strings" ) @@ -24,15 +23,26 @@ const ( ) type chatResponseFull struct { - Channel string `json:"channel"` - Timestamp string `json:"ts"` - Text string `json:"text"` + 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:"user_name"` + Username string `json:"username"` AsUser bool `json:"as_user"` Parse string `json:"parse"` ThreadTimestamp string `json:"thread_ts"` @@ -112,11 +122,10 @@ func (api *Client) PostMessageContext(ctx context.Context, channel, text string, // PostEphemeral sends an ephemeral message to a user in a channel. // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. -func (api *Client) PostEphemeral(channel, userID string, options ...MsgOption) (string, error) { - options = append(options, MsgOptionPostEphemeral()) +func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption) (string, error) { return api.PostEphemeralContext( context.Background(), - channel, + channelID, userID, options..., ) @@ -124,30 +133,19 @@ func (api *Client) PostEphemeral(channel, userID string, options ...MsgOption) ( // PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context // For more details, see PostEphemeral documentation -func (api *Client) PostEphemeralContext(ctx context.Context, channel, userID string, options ...MsgOption) (string, error) { - path, values, err := ApplyMsgOptions(api.token, channel, options...) - if err != nil { - return "", err - } - - values.Add("user", userID) - - response, err := chatRequest(ctx, api.httpclient, path, values, api.debug) - if err != nil { - return "", err - } - - return response.Timestamp, nil +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(channel, timestamp, text string) (string, string, string, error) { - return api.UpdateMessageContext(context.Background(), channel, timestamp, text) +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, channel, timestamp, text string) (string, string, string, error) { - return api.SendMessageContext(ctx, channel, MsgOptionUpdate(timestamp), MsgOptionText(text, true)) +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. @@ -156,22 +154,30 @@ func (api *Client) SendMessage(channel string, options ...MsgOption) (string, st } // SendMessageContext more flexible method for configuring messages with a custom context. -func (api *Client) SendMessageContext(ctx context.Context, channel string, options ...MsgOption) (string, string, string, error) { - channel, values, err := ApplyMsgOptions(api.token, channel, options...) - if err != nil { +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 } - response, err := chatRequest(ctx, api.httpclient, channel, values, api.debug) - if err != nil { + if err = postSlackMethod(ctx, api.httpclient, string(config.mode), config.values, &response, api.debug); err != nil { return "", "", "", err } - return response.Channel, response.Timestamp, response.Text, nil + 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{ @@ -182,11 +188,11 @@ func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.V for _, opt := range options { if err := opt(&config); err != nil { - return string(config.mode), config.values, err + return config, err } } - return string(config.mode), config.values, nil + return config, nil } func escapeMessage(message string) string { @@ -194,18 +200,6 @@ func escapeMessage(message string) string { return replacer.Replace(message) } -func chatRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*chatResponseFull, error) { - response := &chatResponseFull{} - err := post(ctx, client, path, values, response, debug) - if err != nil { - return nil, err - } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil -} - type sendMode string const ( @@ -213,6 +207,7 @@ const ( chatPostMessage sendMode = "chat.postMessage" chatDelete sendMode = "chat.delete" chatPostEphemeral sendMode = "chat.postEphemeral" + chatMeMessage sendMode = "chat.meMessage" ) type sendConfig struct { @@ -232,7 +227,8 @@ func MsgOptionPost() MsgOption { } } -// MsgOptionPostEphemeral posts an ephemeral message +// MsgOptionPostEphemeral - DEPRECATED: use MsgOptionPostEphemeral2 +// posts an ephemeral message. func MsgOptionPostEphemeral() MsgOption { return func(config *sendConfig) error { config.mode = chatPostEphemeral @@ -241,6 +237,25 @@ func MsgOptionPostEphemeral() MsgOption { } } +// 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 { @@ -269,6 +284,14 @@ func MsgOptionAsUser(b bool) MsgOption { } } +// 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 { @@ -328,11 +351,52 @@ func MsgOptionDisableMarkdown() MsgOption { } } +// 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", string(params.Username)) + config.values.Set("username", params.Username) } // chat.postEphemeral support @@ -344,7 +408,7 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { MsgOptionAsUser(params.AsUser)(config) if params.Parse != DEFAULT_MESSAGE_PARSE { - config.values.Set("parse", string(params.Parse)) + config.values.Set("parse", params.Parse) } if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES { config.values.Set("link_names", "1") diff --git a/vendor/github.com/nlopes/slack/conversation.go b/vendor/github.com/nlopes/slack/conversation.go index 26ee292..9e113e0 100644 --- a/vendor/github.com/nlopes/slack/conversation.go +++ b/vendor/github.com/nlopes/slack/conversation.go @@ -29,6 +29,8 @@ type conversation struct { NameNormalized string `json:"name_normalized"` NumMembers int `json:"num_members"` Priority float64 `json:"priority"` + User string `json:"user"` + // TODO support pending_shared // TODO support previous_names } @@ -83,14 +85,14 @@ func (api *Client) GetUsersInConversationContext(ctx context.Context, params *Ge values.Add("cursor", params.Cursor) } if params.Limit != 0 { - values.Add("limit", string(params.Limit)) + values.Add("limit", strconv.Itoa(params.Limit)) } response := struct { Members []string `json:"members"` ResponseMetaData responseMetaData `json:"response_metadata"` SlackResponse }{} - err := post(ctx, api.httpclient, "conversations.members", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.members", values, &response, api.debug) if err != nil { return nil, "", err } @@ -112,14 +114,12 @@ func (api *Client) ArchiveConversationContext(ctx context.Context, channelID str "channel": {channelID}, } response := SlackResponse{} - err := post(ctx, api.httpclient, "conversations.archive", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.archive", values, &response, api.debug) if err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // UnArchiveConversation reverses conversation archival @@ -134,14 +134,12 @@ func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID s "channel": {channelID}, } response := SlackResponse{} - err := post(ctx, api.httpclient, "conversations.unarchive", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.unarchive", values, &response, api.debug) if err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // SetTopicOfConversation sets the topic for a conversation @@ -160,14 +158,12 @@ func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID, SlackResponse Channel *Channel `json:"channel"` }{} - err := post(ctx, api.httpclient, "conversations.setTopic", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.setTopic", values, &response, api.debug) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response.Channel, nil + + return response.Channel, response.Err() } // SetPurposeOfConversation sets the purpose for a conversation @@ -186,14 +182,12 @@ func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelI SlackResponse Channel *Channel `json:"channel"` }{} - err := post(ctx, api.httpclient, "conversations.setPurpose", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.setPurpose", values, &response, api.debug) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response.Channel, nil + + return response.Channel, response.Err() } // RenameConversation renames a conversation @@ -212,14 +206,12 @@ func (api *Client) RenameConversationContext(ctx context.Context, channelID, cha SlackResponse Channel *Channel `json:"channel"` }{} - err := post(ctx, api.httpclient, "conversations.rename", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.rename", values, &response, api.debug) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response.Channel, nil + + return response.Channel, response.Err() } // InviteUsersToConversation invites users to a channel @@ -238,14 +230,12 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel SlackResponse Channel *Channel `json:"channel"` }{} - err := post(ctx, api.httpclient, "conversations.invite", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.invite", values, &response, api.debug) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response.Channel, nil + + return response.Channel, response.Err() } // KickUserFromConversation removes a user from a conversation @@ -261,14 +251,12 @@ func (api *Client) KickUserFromConversationContext(ctx context.Context, channelI "user": {user}, } response := SlackResponse{} - err := post(ctx, api.httpclient, "conversations.kick", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.kick", values, &response, api.debug) if err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // CloseConversation closes a direct message or multi-person direct message @@ -288,14 +276,12 @@ func (api *Client) CloseConversationContext(ctx context.Context, channelID strin AlreadyClosed bool `json:"already_closed"` }{} - err = post(ctx, api.httpclient, "conversations.close", values, &response, api.debug) + err = postSlackMethod(ctx, api.httpclient, "conversations.close", values, &response, api.debug) if err != nil { return false, false, err } - if !response.Ok { - return false, false, errors.New(response.Error) - } - return response.NoOp, response.AlreadyClosed, nil + + return response.NoOp, response.AlreadyClosed, response.Err() } // CreateConversation initiates a public or private channel-based conversation @@ -315,10 +301,8 @@ func (api *Client) CreateConversationContext(ctx context.Context, channelName st if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return &response.Channel, nil + + return &response.Channel, response.Err() } // GetConversationInfo retrieves information about a conversation @@ -338,10 +322,8 @@ func (api *Client) GetConversationInfoContext(ctx context.Context, channelID str if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return &response.Channel, nil + + return &response.Channel, response.Err() } // LeaveConversation leaves a conversation @@ -361,7 +343,7 @@ func (api *Client) LeaveConversationContext(ctx context.Context, channelID strin return false, err } - return response.NotInChannel, nil + return response.NotInChannel, err } type GetConversationRepliesParameters struct { @@ -393,7 +375,7 @@ func (api *Client) GetConversationRepliesContext(ctx context.Context, params *Ge values.Add("latest", params.Latest) } if params.Limit != 0 { - values.Add("limit", string(params.Limit)) + values.Add("limit", strconv.Itoa(params.Limit)) } if params.Oldest != "" { values.Add("oldest", params.Oldest) @@ -412,14 +394,12 @@ func (api *Client) GetConversationRepliesContext(ctx context.Context, params *Ge Messages []Message `json:"messages"` }{} - err = post(ctx, api.httpclient, "conversations.replies", values, &response, api.debug) + err = postSlackMethod(ctx, api.httpclient, "conversations.replies", values, &response, api.debug) if err != nil { return nil, false, "", err } - if !response.Ok { - return nil, false, "", errors.New(response.Error) - } - return response.Messages, response.HasMore, response.ResponseMetaData.NextCursor, nil + + return response.Messages, response.HasMore, response.ResponseMetaData.NextCursor, response.Err() } type GetConversationsParameters struct { @@ -444,7 +424,7 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve values.Add("cursor", params.Cursor) } if params.Limit != 0 { - values.Add("limit", string(params.Limit)) + values.Add("limit", strconv.Itoa(params.Limit)) } if params.Types != nil { values.Add("types", strings.Join(params.Types, ",")) @@ -454,14 +434,12 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve ResponseMetaData responseMetaData `json:"response_metadata"` SlackResponse }{} - err = post(ctx, api.httpclient, "conversations.list", values, &response, api.debug) + err = postSlackMethod(ctx, api.httpclient, "conversations.list", values, &response, api.debug) if err != nil { return nil, "", err } - if !response.Ok { - return nil, "", errors.New(response.Error) - } - return response.Channels, response.ResponseMetaData.NextCursor, nil + + return response.Channels, response.ResponseMetaData.NextCursor, response.Err() } type OpenConversationParameters struct { @@ -493,14 +471,12 @@ func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConv AlreadyOpen bool `json:"already_open"` SlackResponse }{} - err := post(ctx, api.httpclient, "conversations.open", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.open", values, &response, api.debug) if err != nil { return nil, false, false, err } - if !response.Ok { - return nil, false, false, errors.New(response.Error) - } - return response.Channel, response.NoOp, response.AlreadyOpen, nil + + return response.Channel, response.NoOp, response.AlreadyOpen, response.Err() } // JoinConversation joins an existing conversation @@ -519,12 +495,12 @@ func (api *Client) JoinConversationContext(ctx context.Context, channelID string } `json:"response_metadata"` SlackResponse }{} - err := post(ctx, api.httpclient, "conversations.join", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.join", values, &response, api.debug) if err != nil { return nil, "", nil, err } - if !response.Ok { - return nil, "", nil, errors.New(response.Error) + if response.Err() != nil { + return nil, "", nil, response.Err() } var warnings []string if response.ResponseMetaData != nil { @@ -573,7 +549,7 @@ func (api *Client) GetConversationHistoryContext(ctx context.Context, params *Ge values.Add("latest", params.Latest) } if params.Limit != 0 { - values.Add("limit", string(params.Limit)) + values.Add("limit", strconv.Itoa(params.Limit)) } if params.Oldest != "" { values.Add("oldest", params.Oldest) @@ -581,7 +557,7 @@ func (api *Client) GetConversationHistoryContext(ctx context.Context, params *Ge response := GetConversationHistoryResponse{} - err := post(ctx, api.httpclient, "conversations.history", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.history", values, &response, api.debug) if err != nil { return nil, err } 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 index ad8512b..26d36d6 100644 --- a/vendor/github.com/nlopes/slack/dnd.go +++ b/vendor/github.com/nlopes/slack/dnd.go @@ -38,7 +38,7 @@ type dndTeamInfoResponse struct { func dndRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*dndResponseFull, error) { response := &dndResponseFull{} - err := post(ctx, client, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, debug) if err != nil { return nil, err } @@ -61,13 +61,11 @@ func (api *Client) EndDNDContext(ctx context.Context) error { response := &SlackResponse{} - if err := post(ctx, api.httpclient, "dnd.endDnd", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "dnd.endDnd", values, response, api.debug); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // EndSnooze ends the current user's snooze mode @@ -122,7 +120,7 @@ func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (m } response := &dndTeamInfoResponse{} - if err := post(ctx, api.httpclient, "dnd.teamInfo", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "dnd.teamInfo", values, response, api.debug); err != nil { return nil, err } if !response.Ok { diff --git a/vendor/github.com/nlopes/slack/emoji.go b/vendor/github.com/nlopes/slack/emoji.go index c4b3c93..fe2945c 100644 --- a/vendor/github.com/nlopes/slack/emoji.go +++ b/vendor/github.com/nlopes/slack/emoji.go @@ -23,7 +23,7 @@ func (api *Client) GetEmojiContext(ctx context.Context) (map[string]string, erro } response := &emojiResponseFull{} - err := post(ctx, api.httpclient, "emoji.list", values, response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "emoji.list", values, response, api.debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/files.go b/vendor/github.com/nlopes/slack/files.go index 555d3a5..2381ec3 100644 --- a/vendor/github.com/nlopes/slack/files.go +++ b/vendor/github.com/nlopes/slack/files.go @@ -244,9 +244,9 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam 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, SLACK_API+"files.upload", params.File, "file", values, response, api.debug) + err = postLocalWithMultipartResponse(ctx, api.httpclient, "files.upload", params.File, "file", values, response, api.debug) } else if params.Reader != nil { - err = postWithMultipartResponse(ctx, api.httpclient, SLACK_API+"files.upload", params.Filename, "file", values, params.Reader, response, api.debug) + err = postWithMultipartResponse(ctx, api.httpclient, "files.upload", params.Filename, "file", values, params.Reader, response, api.debug) } if err != nil { return nil, err @@ -273,11 +273,8 @@ func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, comment "file": {fileID}, "id": {commentID}, } - if _, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api.debug); err != nil { - return err - } - - return nil + _, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api.debug) + return err } // DeleteFile deletes a file @@ -292,11 +289,8 @@ func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err er "file": {fileID}, } - if _, err = fileRequest(ctx, api.httpclient, "files.delete", values, api.debug); err != nil { - return err - } - - return nil + _, err = fileRequest(ctx, api.httpclient, "files.delete", values, api.debug) + return err } // RevokeFilePublicURL disables public/external sharing for a file diff --git a/vendor/github.com/nlopes/slack/groups.go b/vendor/github.com/nlopes/slack/groups.go index d0e7d91..67e78e9 100644 --- a/vendor/github.com/nlopes/slack/groups.go +++ b/vendor/github.com/nlopes/slack/groups.go @@ -53,9 +53,6 @@ func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error } _, err := groupRequest(ctx, api.httpclient, "groups.archive", values, api.debug) - if err != nil { - return err - } return err } @@ -72,10 +69,7 @@ func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) erro } _, err := groupRequest(ctx, api.httpclient, "groups.unarchive", values, api.debug) - if err != nil { - return err - } - return nil + return err } // CreateGroup creates a private group @@ -215,11 +209,8 @@ func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err err "channel": {group}, } - if _, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug); err != nil { - return err - } - - return nil + _, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug) + return err } // KickUserFromGroup kicks a user from a group @@ -235,11 +226,8 @@ func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user str "user": {user}, } - if _, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug); err != nil { - return err - } - - return nil + _, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug) + return err } // GetGroups retrieves all groups @@ -300,11 +288,8 @@ func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string "ts": {ts}, } - if _, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug); err != nil { - return err - } - - return nil + _, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug) + return err } // OpenGroup opens a private group diff --git a/vendor/github.com/nlopes/slack/im.go b/vendor/github.com/nlopes/slack/im.go index 55b24b7..fa8b095 100644 --- a/vendor/github.com/nlopes/slack/im.go +++ b/vendor/github.com/nlopes/slack/im.go @@ -31,7 +31,7 @@ type IM struct { func imRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*imResponseFull, error) { response := &imResponseFull{} - err := post(ctx, client, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, debug) if err != nil { return nil, err } @@ -87,18 +87,15 @@ func (api *Client) MarkIMChannel(channel, ts string) (err error) { } // MarkIMChannelContext sets the read mark of a direct message channel to a specific point with a custom context -func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string) (err error) { +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) - if err != nil { - return err - } - return + _, err := imRequest(ctx, api.httpclient, "im.mark", values, api.debug) + return err } // GetIMHistory retrieves the direct message channel history diff --git a/vendor/github.com/nlopes/slack/info.go b/vendor/github.com/nlopes/slack/info.go index 49db532..db8534c 100644 --- a/vendor/github.com/nlopes/slack/info.go +++ b/vendor/github.com/nlopes/slack/info.go @@ -1,7 +1,9 @@ package slack import ( + "bytes" "fmt" + "strconv" "time" ) @@ -127,6 +129,19 @@ 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"` @@ -156,7 +171,7 @@ type Info struct { type infoResponseFull struct { Info - WebResponse + SlackResponse } // GetBotByID returns a bot given a bot id diff --git a/vendor/github.com/nlopes/slack/messages.go b/vendor/github.com/nlopes/slack/messages.go index 4d9df61..6551dd4 100644 --- a/vendor/github.com/nlopes/slack/messages.go +++ b/vendor/github.com/nlopes/slack/messages.go @@ -8,6 +8,7 @@ type OutgoingMessage struct { 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 @@ -26,7 +27,7 @@ type Msg struct { Timestamp string `json:"ts,omitempty"` ThreadTimestamp string `json:"thread_ts,omitempty"` IsStarred bool `json:"is_starred,omitempty"` - PinnedTo []string `json:"pinned_to, omitempty"` + PinnedTo []string `json:"pinned_to,omitempty"` Attachments []Attachment `json:"attachments,omitempty"` Edited *Edited `json:"edited,omitempty"` LastRead string `json:"last_read,omitempty"` @@ -68,7 +69,7 @@ type Msg struct { ParentUserId string `json:"parent_user_id,omitempty"` // file_share, file_comment, file_mention - File *File `json:"file,omitempty"` + Files []File `json:"files,omitempty"` // file_share Upload bool `json:"upload,omitempty"` @@ -88,7 +89,8 @@ type Msg struct { // slash commands and interactive messages ResponseType string `json:"response_type,omitempty"` - ReplaceOriginal bool `json:"replace_original,omitempty"` + ReplaceOriginal bool `json:"replace_original"` + DeleteOriginal bool `json:"delete_original"` } // Icon is used for bot messages @@ -116,27 +118,33 @@ type Event struct { // Ping contains information about a Ping Event type Ping struct { - ID int `json:"id"` - Type string `json:"type"` + 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"` + 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) *OutgoingMessage { +func (rtm *RTM) NewOutgoingMessage(text string, channelID string, options ...RTMsgOption) *OutgoingMessage { id := rtm.idGen.Next() - return &OutgoingMessage{ + 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 @@ -150,3 +158,21 @@ func (rtm *RTM) NewTypingMessage(channelID string) *OutgoingMessage { 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 index 32f2367..6910338 100644 --- a/vendor/github.com/nlopes/slack/misc.go +++ b/vendor/github.com/nlopes/slack/misc.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -18,15 +19,41 @@ import ( "time" ) -type WebResponse struct { - Ok bool `json:"ok"` - Error *WebError `json:"error"` +type SlackResponse struct { + Ok bool `json:"ok"` + Error string `json:"error"` } -type WebError string +func (t SlackResponse) Err() error { + if t.Ok { + return nil + } -func (s WebError) Error() string { - return string(s) + // 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 { @@ -63,7 +90,7 @@ func fileUploadReq(ctx context.Context, path, fieldname, filename string, values return req, nil } -func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error { +func parseResponseBody(body io.ReadCloser, intf interface{}, debug bool) error { response, err := ioutil.ReadAll(body) if err != nil { return err @@ -74,7 +101,7 @@ func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error logger.Printf("parseResponseBody: %s\n", string(response)) } - return json.Unmarshal(response, &intf) + return json.Unmarshal(response, intf) } func postLocalWithMultipartResponse(ctx context.Context, client HTTPRequester, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error { @@ -113,20 +140,13 @@ func postWithMultipartResponse(ctx context.Context, client HTTPRequester, path, // 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 fmt.Errorf("Slack server error: %s.", resp.Status) + return statusCodeError{Code: resp.StatusCode, Status: resp.Status} } - return parseResponseBody(resp.Body, &intf, debug) + return parseResponseBody(resp.Body, 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") - +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 { @@ -145,13 +165,37 @@ func postForm(ctx context.Context, client HTTPRequester, endpoint string, values // 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 fmt.Errorf("Slack server error: %s.", resp.Status) + return statusCodeError{Code: resp.StatusCode, Status: resp.Status} } - return parseResponseBody(resp.Body, &intf, debug) + return parseResponseBody(resp.Body, intf, debug) } -func post(ctx context.Context, client HTTPRequester, path string, values url.Values, intf interface{}, debug bool) error { +// post JSON. +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) +} + +// post a url encoded form. +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) +} + +// post to a slack web method. +func postSlackMethod(ctx context.Context, client HTTPRequester, path string, values url.Values, intf interface{}, debug bool) error { return postForm(ctx, client, SLACK_API+path, values, intf, debug) } @@ -173,10 +217,24 @@ func logResponse(resp *http.Response, debug bool) error { return nil } -func okJsonHandler(rw http.ResponseWriter, r *http.Request) { +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 index 0af190e..378af4a 100644 --- a/vendor/github.com/nlopes/slack/oauth.go +++ b/vendor/github.com/nlopes/slack/oauth.go @@ -55,7 +55,7 @@ func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code, "redirect_uri": {redirectURI}, } response := &OAuthResponse{} - err = post(ctx, customHTTPClient, "oauth.access", values, response, debug) + err = postSlackMethod(ctx, customHTTPClient, "oauth.access", values, response, debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/pins.go b/vendor/github.com/nlopes/slack/pins.go index 6b39778..34863f1 100644 --- a/vendor/github.com/nlopes/slack/pins.go +++ b/vendor/github.com/nlopes/slack/pins.go @@ -24,23 +24,21 @@ func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemR "token": {api.token}, } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } response := &SlackResponse{} - if err := post(ctx, api.httpclient, "pins.add", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "pins.add", values, response, api.debug); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // RemovePin un-pins an item from a channel @@ -55,23 +53,21 @@ func (api *Client) RemovePinContext(ctx context.Context, channel string, item It "token": {api.token}, } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } response := &SlackResponse{} - if err := post(ctx, api.httpclient, "pins.remove", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "pins.remove", values, response, api.debug); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // ListPins returns information about the items a user reacted to. @@ -87,7 +83,7 @@ func (api *Client) ListPinsContext(ctx context.Context, channel string) ([]Item, } response := &listPinsResponseFull{} - err := post(ctx, api.httpclient, "pins.list", values, response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "pins.list", values, response, api.debug) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/reactions.go b/vendor/github.com/nlopes/slack/reactions.go index c0556d8..5eabde6 100644 --- a/vendor/github.com/nlopes/slack/reactions.go +++ b/vendor/github.com/nlopes/slack/reactions.go @@ -142,26 +142,24 @@ func (api *Client) AddReactionContext(ctx context.Context, name string, item Ite values.Set("name", name) } if item.Channel != "" { - values.Set("channel", string(item.Channel)) + values.Set("channel", item.Channel) } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } response := &SlackResponse{} - if err := post(ctx, api.httpclient, "reactions.add", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "reactions.add", values, response, api.debug); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // RemoveReaction removes a reaction emoji from a message, file or file comment. @@ -178,26 +176,24 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item values.Set("name", name) } if item.Channel != "" { - values.Set("channel", string(item.Channel)) + values.Set("channel", item.Channel) } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } response := &SlackResponse{} - if err := post(ctx, api.httpclient, "reactions.remove", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "reactions.remove", values, response, api.debug); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // GetReactions returns details about the reactions on an item. @@ -211,23 +207,23 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params "token": {api.token}, } if item.Channel != "" { - values.Set("channel", string(item.Channel)) + values.Set("channel", item.Channel) } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(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 := post(ctx, api.httpclient, "reactions.get", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "reactions.get", values, response, api.debug); err != nil { return nil, err } if !response.Ok { @@ -260,7 +256,7 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction } response := &listReactionsResponseFull{} - err := post(ctx, api.httpclient, "reactions.list", values, response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "reactions.list", values, response, api.debug) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/rtm.go b/vendor/github.com/nlopes/slack/rtm.go index 7b55c2a..41a136e 100644 --- a/vendor/github.com/nlopes/slack/rtm.go +++ b/vendor/github.com/nlopes/slack/rtm.go @@ -3,13 +3,24 @@ package slack import ( "context" "encoding/json" - "fmt" "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. @@ -27,15 +38,13 @@ func (api *Client) StartRTM() (info *Info, websocketURL string, err error) { // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) { response := &infoResponseFull{} - err = post(ctx, api.httpclient, "rtm.start", url.Values{"token": {api.token}}, response, api.debug) + err = postSlackMethod(ctx, api.httpclient, "rtm.start", url.Values{"token": {api.token}}, response, api.debug) if err != nil { - return nil, "", fmt.Errorf("post: %s", err) - } - if !response.Ok { - return nil, "", response.Error + return nil, "", err } + api.Debugln("Using URL:", response.Info.URL) - return &response.Info, response.Info.URL, nil + return &response.Info, response.Info.URL, response.Err() } // ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block. @@ -48,52 +57,82 @@ func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) { return api.ConnectRTMContext(ctx) } -// ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block with a custom context. +// 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 = post(ctx, api.httpclient, "rtm.connect", url.Values{"token": {api.token}}, response, api.debug) + err = postSlackMethod(ctx, api.httpclient, "rtm.connect", url.Values{"token": {api.token}}, response, api.debug) if err != nil { api.Debugf("Failed to connect to RTM: %s", err) - return nil, "", fmt.Errorf("post: %s", err) - } - if !response.Ok { - return nil, "", response.Error + return nil, "", err } + api.Debugln("Using URL:", response.Info.URL) - return &response.Info, response.Info.URL, nil + 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() *RTM { - return api.NewRTMWithOptions(nil) -} - -// NewRTMWithOptions 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 { +func (api *Client) NewRTM(options ...RTMOption) *RTM { result := &RTM{ Client: *api, IncomingEvents: make(chan RTMEvent, 50), outgoingMessages: make(chan OutgoingMessage, 20), - pings: make(map[int]time.Time), + pingInterval: defaultPingInterval, + pingDeadman: time.NewTimer(deadmanDuration(defaultPingInterval)), isConnected: false, wasIntentional: true, killChannel: make(chan bool), - disconnected: make(chan struct{}), + disconnected: make(chan struct{}, 1), forcePing: make(chan bool), rawEvents: make(chan json.RawMessage), idGen: NewSafeID(1), + mu: &sync.Mutex{}, } - if options != nil { - result.useRTMStart = options.UseRTMStart - } else { - result.useRTMStart = true + 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 index 390dcdb..e858952 100644 --- a/vendor/github.com/nlopes/slack/search.go +++ b/vendor/github.com/nlopes/slack/search.go @@ -11,7 +11,7 @@ const ( DEFAULT_SEARCH_SORT = "score" DEFAULT_SEARCH_SORT_DIR = "desc" DEFAULT_SEARCH_HIGHLIGHT = false - DEFAULT_SEARCH_COUNT = 100 + DEFAULT_SEARCH_COUNT = 20 DEFAULT_SEARCH_PAGE = 1 ) @@ -37,17 +37,18 @@ type CtxMessage struct { } 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"` - Previous CtxMessage `json:"previous"` - Previous2 CtxMessage `json:"previous_2"` - Next CtxMessage `json:"next"` - Next2 CtxMessage `json:"next_2"` + 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 { @@ -103,7 +104,7 @@ func (api *Client) _search(ctx context.Context, path, query string, params Searc } response = &searchResponseFull{} - err := post(ctx, api.httpclient, path, values, response, api.debug) + err := postSlackMethod(ctx, api.httpclient, path, values, response, api.debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/slack.go b/vendor/github.com/nlopes/slack/slack.go index ddf42e9..6d1e7de 100644 --- a/vendor/github.com/nlopes/slack/slack.go +++ b/vendor/github.com/nlopes/slack/slack.go @@ -35,9 +35,17 @@ func SetHTTPClient(client HTTPRequester) { customHTTPClient = client } -type SlackResponse struct { - Ok bool `json:"ok"` - Error string `json:"error"` +// 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 { @@ -93,7 +101,7 @@ func (api *Client) AuthTest() (response *AuthTestResponse, error error) { func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestResponse, error error) { api.Debugf("Challenging auth...") responseFull := &authTestResponseFull{} - err := post(ctx, api.httpclient, "auth.test", url.Values{"token": {api.token}}, responseFull, api.debug) + err := postSlackMethod(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 diff --git a/vendor/github.com/nlopes/slack/slash.go b/vendor/github.com/nlopes/slack/slash.go index c21a478..f62065a 100644 --- a/vendor/github.com/nlopes/slack/slash.go +++ b/vendor/github.com/nlopes/slack/slash.go @@ -6,17 +6,19 @@ import ( // SlashCommand contains information about a request of the slash command type SlashCommand struct { - Token string `json:"token"` - TeamID string `json:"team_id"` - TeamDomain string `json:"team_domain"` - ChannelID string `json:"channel_id"` - ChannelName string `json:"channel_name"` - UserID string `json:"user_id"` - UserName string `json:"user_name"` - Command string `json:"command"` - Text string `json:"text"` - ResponseURL string `json:"response_url"` - TriggerID string `json:"trigger_id"` + 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 @@ -27,6 +29,8 @@ func SlashCommandParse(r *http.Request) (s SlashCommand, err error) { 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") diff --git a/vendor/github.com/nlopes/slack/stars.go b/vendor/github.com/nlopes/slack/stars.go index 785dec5..c1e2f6c 100644 --- a/vendor/github.com/nlopes/slack/stars.go +++ b/vendor/github.com/nlopes/slack/stars.go @@ -48,23 +48,21 @@ func (api *Client) AddStarContext(ctx context.Context, channel string, item Item "token": {api.token}, } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } response := &SlackResponse{} - if err := post(ctx, api.httpclient, "stars.add", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "stars.add", values, response, api.debug); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // RemoveStar removes a starred item from a channel @@ -79,23 +77,21 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I "token": {api.token}, } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } response := &SlackResponse{} - if err := post(ctx, api.httpclient, "stars.remove", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "stars.remove", values, response, api.debug); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // ListStars returns information about the stars a user added @@ -119,7 +115,7 @@ func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) } response := &listResponseFull{} - err := post(ctx, api.httpclient, "stars.list", values, response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "stars.list", values, response, api.debug) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/team.go b/vendor/github.com/nlopes/slack/team.go index 53ba572..b6e341e 100644 --- a/vendor/github.com/nlopes/slack/team.go +++ b/vendor/github.com/nlopes/slack/team.go @@ -69,7 +69,7 @@ func NewAccessLogParameters() AccessLogParameters { func teamRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*TeamResponse, error) { response := &TeamResponse{} - err := post(ctx, client, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, debug) if err != nil { return nil, err } @@ -83,7 +83,7 @@ func teamRequest(ctx context.Context, client HTTPRequester, path string, values func billableInfoRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (map[string]BillingActive, error) { response := &BillableInfoResponse{} - err := post(ctx, client, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, debug) if err != nil { return nil, err } @@ -97,7 +97,7 @@ func billableInfoRequest(ctx context.Context, client HTTPRequester, path string, func accessLogsRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*LoginResponse, error) { response := &LoginResponse{} - err := post(ctx, client, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/usergroups.go b/vendor/github.com/nlopes/slack/usergroups.go index 4abcf0c..1e2b644 100644 --- a/vendor/github.com/nlopes/slack/usergroups.go +++ b/vendor/github.com/nlopes/slack/usergroups.go @@ -42,7 +42,7 @@ type userGroupResponseFull struct { func userGroupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userGroupResponseFull, error) { response := &userGroupResponseFull{} - err := post(ctx, client, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/users.go b/vendor/github.com/nlopes/slack/users.go index 5b3dddc..0dd20db 100644 --- a/vendor/github.com/nlopes/slack/users.go +++ b/vendor/github.com/nlopes/slack/users.go @@ -5,37 +5,97 @@ import ( "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"` + 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 @@ -108,10 +168,11 @@ type TeamIdentity struct { } type userResponseFull struct { - Members []User `json:"members,omitempty"` // ListUsers - User `json:"user,omitempty"` // GetUserInfo - UserPresence // GetUserPresence + Members []User `json:"members,omitempty"` + User `json:"user,omitempty"` + UserPresence SlackResponse + Metadata ResponseMetadata `json:"response_metadata"` } type UserSetPhotoParams struct { @@ -178,23 +239,109 @@ func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, 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) ([]User, error) { - values := url.Values{ - "token": {api.token}, - "presence": {"1"}, +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...) } - response, err := userRequest(ctx, api.httpclient, "users.list", values, api.debug) - if err != nil { - return nil, err - } - return response.Members, nil + return results, p.Failure(err) } // GetUserByEmail will retrieve the complete user information by email @@ -226,11 +373,8 @@ func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) { "token": {api.token}, } - if _, err := userRequest(ctx, api.httpclient, "users.setActive", values, api.debug); err != nil { - return err - } - - return nil + _, err = userRequest(ctx, api.httpclient, "users.setActive", values, api.debug) + return err } // SetUserPresence changes the currently authenticated user presence @@ -246,11 +390,7 @@ func (api *Client) SetUserPresenceContext(ctx context.Context, presence string) } _, err := userRequest(ctx, api.httpclient, "users.setPresence", values, api.debug) - if err != nil { - return err - } - return nil - + return err } // GetUserIdentity will retrieve user info available per identity scopes @@ -287,23 +427,21 @@ func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params "token": {api.token}, } if params.CropX != DEFAULT_USER_PHOTO_CROP_X { - values.Add("crop_x", string(params.CropX)) + values.Add("crop_x", strconv.Itoa(params.CropX)) } if params.CropY != DEFAULT_USER_PHOTO_CROP_Y { - values.Add("crop_y", string(params.CropY)) + values.Add("crop_y", strconv.Itoa(params.CropX)) } if params.CropW != DEFAULT_USER_PHOTO_CROP_W { - values.Add("crop_w", string(params.CropW)) + values.Add("crop_w", strconv.Itoa(params.CropW)) } - err := postLocalWithMultipartResponse(ctx, api.httpclient, SLACK_API+"users.setPhoto", image, "image", values, response, api.debug) + err := postLocalWithMultipartResponse(ctx, api.httpclient, "users.setPhoto", image, "image", values, response, api.debug) if err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // DeleteUserPhoto deletes the current authenticated user's profile image @@ -322,10 +460,8 @@ func (api *Client) DeleteUserPhotoContext(ctx context.Context) error { if err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // SetUserCustomStatus will set a custom status and emoji for the currently @@ -392,3 +528,31 @@ func (api *Client) UnsetUserCustomStatus() error { 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 := postSlackMethod(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/webhooks.go b/vendor/github.com/nlopes/slack/webhooks.go new file mode 100644 index 0000000..870a8d8 --- /dev/null +++ b/vendor/github.com/nlopes/slack/webhooks.go @@ -0,0 +1,33 @@ +package slack + +import ( + "github.com/pkg/errors" + "net/http" + "bytes" + "encoding/json" +) + +type WebhookMessage struct { + Text string `json:"text,omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` +} + +func PostWebhook(url string, msg *WebhookMessage) error { + raw, err := json.Marshal(msg) + + if err != nil { + return errors.Wrap(err, "marshal failed") + } + + response, err := http.Post(url, "application/json", bytes.NewReader(raw)); + + if err != nil { + return errors.Wrap(err, "failed to post webhook") + } + + if response.StatusCode != http.StatusOK { + return statusCodeError{Code: response.StatusCode, Status: response.Status} + } + + return nil +} diff --git a/vendor/github.com/nlopes/slack/websocket.go b/vendor/github.com/nlopes/slack/websocket.go index f28c945..242acf4 100644 --- a/vendor/github.com/nlopes/slack/websocket.go +++ b/vendor/github.com/nlopes/slack/websocket.go @@ -3,6 +3,7 @@ package slack import ( "encoding/json" "errors" + "sync" "time" "github.com/gorilla/websocket" @@ -19,8 +20,9 @@ const ( // // Create this element with Client's NewRTM() or NewRTMWithOptions(*RTMOptions) type RTM struct { - idGen IDGenerator - pings map[int]time.Time + idGen IDGenerator + pingInterval time.Duration + pingDeadman *time.Timer // Connection life-cycle conn *websocket.Conn @@ -44,6 +46,13 @@ type RTM struct { // 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 @@ -60,9 +69,17 @@ type RTMOptions struct { // Disconnect and wait, blocking until a successful disconnection. func (rtm *RTM) Disconnect() error { - // this channel is always closed on disconnect. lets the ManagedConnection() function - // properly clean up. - close(rtm.disconnected) + // avoid RTM disconnect race conditions + rtm.mu.Lock() + defer rtm.mu.Unlock() + + // always push into the disconnected channel when invoked, + // this lets the ManagedConnection() function properly clean up. + // if the buffer is full then just continue on. + select { + case rtm.disconnected <- struct{}{}: + default: + } if !rtm.isConnected { return errors.New("Invalid call to Disconnect - Slack API is already disconnected") @@ -72,12 +89,6 @@ func (rtm *RTM) Disconnect() error { return nil } -// Reconnect only makes sense if you've successfully disconnectd with Disconnect(). -func (rtm *RTM) Reconnect() error { - logger.Println("RTM::Reconnect not implemented!") - 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 @@ -97,3 +108,11 @@ func (rtm *RTM) SendMessage(msg *OutgoingMessage) { 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_managed_conn.go b/vendor/github.com/nlopes/slack/websocket_managed_conn.go index 7f7f353..b6d1bfc 100644 --- a/vendor/github.com/nlopes/slack/websocket_managed_conn.go +++ b/vendor/github.com/nlopes/slack/websocket_managed_conn.go @@ -25,27 +25,34 @@ import ( // // The defined error events are located in websocket_internals.go. func (rtm *RTM) ManageConnection() { - var connectionCount int - for { - connectionCount++ + var ( + err error + info *Info + conn *websocket.Conn + ) + + for connectionCount := 0; ; connectionCount++ { // start trying to connect // the returned err is already passed onto the IncomingEvents channel - info, conn, err := rtm.connect(connectionCount, rtm.useRTMStart) - // if err != nil then the connection is sucessful - otherwise it is - // fatal - if err != nil { + 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) return } + + // lock to prevent data races with Disconnect particularly around isConnected + // and conn. + rtm.mu.Lock() + rtm.conn = conn + rtm.isConnected = true rtm.info = info + rtm.mu.Unlock() + rtm.IncomingEvents <- RTMEvent{"connected", &ConnectedEvent{ ConnectionCount: connectionCount, Info: info, }} - rtm.conn = conn - rtm.isConnected = true - rtm.Debugf("RTM connection succeeded on try %d", connectionCount) keepRunning := make(chan bool) @@ -54,7 +61,7 @@ func (rtm *RTM) ManageConnection() { go rtm.handleIncomingEvents(keepRunning) // this should be a blocking call until the connection has ended - rtm.handleEvents(keepRunning, 30*time.Second) + rtm.handleEvents(keepRunning) // after being disconnected we need to check if it was intentional // if not then we should try to reconnect @@ -71,6 +78,12 @@ func (rtm *RTM) ManageConnection() { // 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{ @@ -91,11 +104,14 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke if err == nil { return info, conn, nil } - // check for fatal errors - currently only invalid_auth - if sErr, ok := err.(*WebError); ok && (sErr.Error() == "invalid_auth" || sErr.Error() == "account_inactive") { + + // 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, sErr + return nil, nil, err + default: } // any other errors are treated as recoverable and we try again after @@ -107,7 +123,7 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke // check if Disconnect() has been invoked. select { - case _ = <-rtm.disconnected: + case <-rtm.disconnected: rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: true}} return nil, nil, fmt.Errorf("disconnect received while trying to connect") default: @@ -124,10 +140,10 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke // 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, *websocket.Conn, error) { - var info *Info - var url string - var err error +func (rtm *RTM) startRTMAndDial(useRTMStart bool) (info *Info, _ *websocket.Conn, err error) { + var ( + url string + ) if useRTMStart { rtm.Debugf("Starting RTM") @@ -145,7 +161,11 @@ func (rtm *RTM) startRTMAndDial(useRTMStart bool) (*Info, *websocket.Conn, error // Only use HTTPS for connections to prevent MITM attacks on the connection. upgradeHeader := http.Header{} upgradeHeader.Add("Origin", "https://api.slack.com") - conn, _, err := websocket.DefaultDialer.Dial(url, upgradeHeader) + 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 @@ -175,8 +195,8 @@ func (rtm *RTM) killConnection(keepRunning chan bool, intentional bool) error { // interval. This also sends outgoing messages that are received from the RTM's // outgoingMessages channel. This also handles incoming raw events from the RTM // rawEvents channel. -func (rtm *RTM) handleEvents(keepRunning chan bool, interval time.Duration) { - ticker := time.NewTicker(interval) +func (rtm *RTM) handleEvents(keepRunning chan bool) { + ticker := time.NewTicker(rtm.pingInterval) defer ticker.Stop() for { select { @@ -184,7 +204,12 @@ func (rtm *RTM) handleEvents(keepRunning chan bool, interval time.Duration) { case intentional := <-rtm.killChannel: _ = rtm.killConnection(keepRunning, intentional) return - // send pings on ticker interval + + // 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 { @@ -202,7 +227,11 @@ func (rtm *RTM) handleEvents(keepRunning chan bool, interval time.Duration) { rtm.sendOutgoingMessage(msg) // listen for incoming messages that need to be parsed case rawEvent := <-rtm.rawEvents: - rtm.handleRawEvent(rawEvent) + switch rtm.handleRawEvent(rawEvent) { + case rtmEventTypeGoodbye: + _ = rtm.killConnection(keepRunning, false) + default: + } } } } @@ -220,7 +249,9 @@ func (rtm *RTM) handleIncomingEvents(keepRunning <-chan bool) { case <-keepRunning: return default: - rtm.receiveIncomingEvent() + if err := rtm.receiveIncomingEvent(); err != nil { + return + } } } } @@ -270,9 +301,7 @@ func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) { func (rtm *RTM) ping() error { id := rtm.idGen.Next() rtm.Debugln("Sending PING ", id) - rtm.pings[id] = time.Now() - - msg := &Ping{ID: id, Type: "ping"} + 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()) @@ -283,52 +312,62 @@ func (rtm *RTM) ping() error { // receiveIncomingEvent attempts to receive an event from the RTM's websocket. // This will block until a frame is available from the websocket. -func (rtm *RTM) receiveIncomingEvent() { +// 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) - if err == io.EOF { + 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 pontential websocket disconnect + // trigger a 'PING' to detect potential websocket disconnect rtm.forcePing <- true - return - } else if err != nil { + 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, }} - // force a ping here too? - return - } else if len(event) == 0 { + rtm.killChannel <- false + return err + case len(event) == 0: rtm.Debugln("Received empty event") - return + default: + rtm.Debugln("Incoming Event:", string(event[:])) + rtm.rawEvents <- event } - 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. -func (rtm *RTM) handleRawEvent(rawEvent json.RawMessage) { +// 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 + return "" } + switch event.Type { - case "": + case rtmEventTypeAck: rtm.handleAck(rawEvent) - case "hello": + case rtmEventTypeHello: rtm.IncomingEvents <- RTMEvent{"hello", &HelloEvent{}} - case "pong": + case rtmEventTypePong: rtm.handlePong(rawEvent) - case "desktop_notification": + 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. @@ -359,19 +398,20 @@ func (rtm *RTM) handleAck(event json.RawMessage) { // a previously sent 'PING' message. This is then used to compute the // connection's latency. func (rtm *RTM) handlePong(event json.RawMessage) { - pong := &Pong{} - if err := json.Unmarshal(event, pong); err != nil { - rtm.Debugln("RTM Error unmarshalling 'pong' event:", err) + 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 } - if pingTime, exists := rtm.pings[pong.ReplyTo]; exists { - latency := time.Since(pingTime) - rtm.IncomingEvents <- RTMEvent{"latency_report", &LatencyReport{Value: latency}} - delete(rtm.pings, pong.ReplyTo) - } else { - rtm.Debugln("RTM Error - unmatched 'pong' event:", string(event)) - } + + 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 @@ -381,7 +421,7 @@ func (rtm *RTM) handlePong(event json.RawMessage) { // correct struct then this sends an UnmarshallingErrorEvent to the // IncomingEvents channel. func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { - v, exists := eventMapping[typeStr] + 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)) @@ -400,10 +440,10 @@ func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { rtm.IncomingEvents <- RTMEvent{typeStr, recvEvent} } -// eventMapping holds a mapping of event names to their corresponding struct +// 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{}{ +var EventMapping = map[string]interface{}{ "message": MessageEvent{}, "presence_change": PresenceChangeEvent{}, "user_typing": UserTypingEvent{}, @@ -481,4 +521,7 @@ var eventMapping = map[string]interface{}{ "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 index ad283ea..16f48c7 100644 --- a/vendor/github.com/nlopes/slack/websocket_misc.go +++ b/vendor/github.com/nlopes/slack/websocket_misc.go @@ -80,7 +80,7 @@ type EmojiChangedEvent struct { SubType string `json:"subtype"` Name string `json:"name"` Names []string `json:"names"` - Value string `json:"value"` + Value string `json:"value"` EventTimestamp string `json:"event_ts"` } @@ -119,3 +119,22 @@ 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/nsf/termbox-go/README.md b/vendor/github.com/nsf/termbox-go/README.md index fcc493d..f4b805f 100644 --- a/vendor/github.com/nsf/termbox-go/README.md +++ b/vendor/github.com/nsf/termbox-go/README.md @@ -11,7 +11,6 @@ For examples of what can be done take a look at demos in the _demos directory. Y There are also some interesting projects using termbox-go: - [godit](https://github.com/nsf/godit) is an emacsish lightweight text editor written using termbox. - - [gomatrix](https://github.com/GeertJohan/gomatrix) connects to The Matrix and displays its data streams in your terminal. - [gotetris](https://github.com/jjinux/gotetris) is an implementation of Tetris. - [sokoban-go](https://github.com/rn2dy/sokoban-go) is an implementation of sokoban game. - [hecate](https://github.com/evanmiller/hecate) is a hex editor designed by Satan. @@ -36,3 +35,10 @@ There are also some interesting projects using termbox-go: - [pinger](https://github.com/hirose31/pinger) helps you to monitor numerous hosts using ICMP ECHO_REQUEST. - [vixl44](https://github.com/sebashwa/vixl44) lets you create pixel art inside your terminal using vim movements - [zterm](https://github.com/varunrau/zterm) is a typing game inspired by http://zty.pe/ + - [gotypist](https://github.com/pb-/gotypist) is a fun touch-typing tutor following Steve Yegge's method. + - [cointop](https://github.com/miguelmota/cointop) is an interactive terminal based UI application for tracking cryptocurrencies. + - [pexpo](https://github.com/nnao45/pexpo) is a terminal sending ping tool written in Go. + - [jid](https://github.com/simeji/jid) is an interactive JSON drill down tool using filtering queries like jq. + +### API reference +[godoc.org/github.com/nsf/termbox-go](http://godoc.org/github.com/nsf/termbox-go) diff --git a/vendor/github.com/nsf/termbox-go/termbox_windows.go b/vendor/github.com/nsf/termbox-go/termbox_windows.go index 738847c..7752a17 100644 --- a/vendor/github.com/nsf/termbox-go/termbox_windows.go +++ b/vendor/github.com/nsf/termbox-go/termbox_windows.go @@ -1,5 +1,6 @@ package termbox +import "math" import "syscall" import "unsafe" import "unicode/utf16" @@ -57,6 +58,10 @@ type ( control_key_state dword event_flags dword } + console_font_info struct { + font uint32 + font_size coord + } ) const ( @@ -94,6 +99,7 @@ var ( proc_create_event = kernel32.NewProc("CreateEventW") proc_wait_for_multiple_objects = kernel32.NewProc("WaitForMultipleObjects") proc_set_event = kernel32.NewProc("SetEvent") + proc_get_current_console_font = kernel32.NewProc("GetCurrentConsoleFont") get_system_metrics = moduser32.NewProc("GetSystemMetrics") ) @@ -339,6 +345,19 @@ func set_event(ev syscall.Handle) (err error) { return } +func get_current_console_font(h syscall.Handle, info *console_font_info) (err error) { + r0, _, e1 := syscall.Syscall(proc_get_current_console_font.Addr(), + 3, uintptr(h), 0, uintptr(unsafe.Pointer(info))) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + type diff_msg struct { pos short lines short @@ -383,6 +402,7 @@ var ( tmp_coord0 = coord{0, 0} tmp_coord = coord{0, 0} tmp_rect = small_rect{0, 0, 0, 0} + tmp_finfo console_font_info ) func get_cursor_position(out syscall.Handle) coord { @@ -411,9 +431,14 @@ func get_win_min_size(out syscall.Handle) coord { } } + err1 := get_current_console_font(out, &tmp_finfo) + if err1 != nil { + panic(err1) + } + return coord{ - x: short(x), - y: short(y), + x: short(math.Ceil(float64(x) / float64(tmp_finfo.font_size.x))), + y: short(math.Ceil(float64(y) / float64(tmp_finfo.font_size.y))), } } @@ -442,8 +467,9 @@ func get_win_size(out syscall.Handle) coord { } func update_size_maybe() { - size := get_term_size(out) + size := get_win_size(out) if size.x != term_size.x || size.y != term_size.y { + set_console_screen_buffer_size(out, size) term_size = size back_buffer.resize(int(size.x), int(size.y)) front_buffer.resize(int(size.x), int(size.y)) diff --git a/vendor/github.com/nsf/termbox-go/terminfo.go b/vendor/github.com/nsf/termbox-go/terminfo.go index 5d38fce..ab2e7a1 100644 --- a/vendor/github.com/nsf/termbox-go/terminfo.go +++ b/vendor/github.com/nsf/termbox-go/terminfo.go @@ -69,6 +69,12 @@ func load_terminfo() ([]byte, error) { } } + // next, /lib/terminfo + data, err = ti_try_path("/lib/terminfo") + if err == nil { + return data, nil + } + // fall back to /usr/share/terminfo return ti_try_path("/usr/share/terminfo") } 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 +} diff --git a/vendor/github.com/renstrom/fuzzysearch/LICENSE b/vendor/github.com/renstrom/fuzzysearch/LICENSE index 9cc7533..dee3d1d 100644 --- a/vendor/github.com/renstrom/fuzzysearch/LICENSE +++ b/vendor/github.com/renstrom/fuzzysearch/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Peter Renström +Copyright (c) 2018 Peter Lithammer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go b/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go index 6850e6c..33d4898 100644 --- a/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go +++ b/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go @@ -112,31 +112,34 @@ Outer: } // Count up remaining char - for len(target) > 0 { - target = target[utf8.RuneLen(rune(target[0])):] - runeDiff++ - } + runeDiff += utf8.RuneCountInString(target) return runeDiff } // RankFind is similar to Find, except it will also rank all matches using // Levenshtein distance. -func RankFind(source string, targets []string) ranks { - var r ranks - for _, target := range find(source, targets, noop) { - distance := LevenshteinDistance(source, target) - r = append(r, Rank{source, target, distance}) +func RankFind(source string, targets []string) Ranks { + var r Ranks + + for index, target := range targets { + if match(source, target, noop) { + distance := LevenshteinDistance(source, target) + r = append(r, Rank{source, target, distance, index}) + } } return r } // RankFindFold is a case-insensitive version of RankFind. -func RankFindFold(source string, targets []string) ranks { - var r ranks - for _, target := range find(source, targets, unicode.ToLower) { - distance := LevenshteinDistance(source, target) - r = append(r, Rank{source, target, distance}) +func RankFindFold(source string, targets []string) Ranks { + var r Ranks + + for index, target := range targets { + if match(source, target, unicode.ToLower) { + distance := LevenshteinDistance(source, target) + r = append(r, Rank{source, target, distance, index}) + } } return r } @@ -150,18 +153,21 @@ type Rank struct { // Distance is the Levenshtein distance between Source and Target. Distance int + + // Location of Target in original list + OriginalIndex int } -type ranks []Rank +type Ranks []Rank -func (r ranks) Len() int { +func (r Ranks) Len() int { return len(r) } -func (r ranks) Swap(i, j int) { +func (r Ranks) Swap(i, j int) { r[i], r[j] = r[j], r[i] } -func (r ranks) Less(i, j int) bool { +func (r Ranks) Less(i, j int) bool { return r[i].Distance < r[j].Distance } diff --git a/views/view.go b/views/view.go index 3361616..44e0c4b 100644 --- a/views/view.go +++ b/views/view.go @@ -17,7 +17,7 @@ type View struct { Debug *components.Debug } -func CreateView(config *config.Config, svc *service.SlackService) *View { +func CreateView(config *config.Config, svc *service.SlackService) (*View, error) { // Create Input component input := components.CreateInputComponent() @@ -25,25 +25,32 @@ func CreateView(config *config.Config, svc *service.SlackService) *View { channels := components.CreateChannelsComponent(input.Par.Height) // Channels: fill the component - slackChans := svc.GetChannels() + slackChans, err := svc.GetChannels() + if err != nil { + return nil, err + } + + // Channels: set channels in component channels.SetChannels(slackChans) // Chat: create the component chat := components.CreateChatComponent(input.Par.Height) // Chat: fill the component - msgs := svc.GetMessages( - svc.GetSlackChannel(channels.SelectedChannel), + msgs, err := svc.GetMessages( + channels.ChannelItems[channels.SelectedChannel].ID, chat.GetMaxItems(), ) - - var strMsgs []string - for _, msg := range msgs { - strMsgs = append(strMsgs, msg.ToString()) + if err != nil { + return nil, err } - chat.SetMessages(strMsgs) - chat.SetBorderLabel(svc.Channels[channels.SelectedChannel].GetChannelName()) + // Chat: set messages in component + chat.SetMessages(msgs) + + chat.SetBorderLabel( + channels.ChannelItems[channels.SelectedChannel].GetChannelName(), + ) // Debug: create the component debug := components.CreateDebugComponent(input.Par.Height) @@ -60,7 +67,7 @@ func CreateView(config *config.Config, svc *service.SlackService) *View { Debug: debug, } - return view + return view, nil } func (v *View) Refresh() {