Merge branch 'v0.4.1'

* v0.4.1: (26 commits)
  Update screenshot
  Update README.md
  Add some return errors
  Fix scrolling, fix help page
  Reset event.go
  Fix theme mapping
  Fix hide mpim
  Fix theme colors in view
  Fix searching
  Fix sorting of buckets
  Speed up presence discovery for im channels
  Trying out a sorting mechanism
  Fix user presence
  Add deleted user check
  Remove components dependency out of slack.go
  Start with conversations api
  Fix character width calculation in chat pane
  Add individual name colors
  Add optional emoji support
  Add optional date/time formatting
  ...
This commit is contained in:
erroneousboat 2018-10-27 15:34:28 +02:00
commit 757850de77
81 changed files with 3450 additions and 1409 deletions

56
Gopkg.lock generated
View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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",
"<previous>": "chat-up",
"C-b": "chat-up",
"C-u": "chat-up",
"<next>": "chat-down",
"C-f": "chat-down",
"C-d": "chat-down",
"n": "channel-search-next",
"N": "channel-search-previous",
"q": "quit",
"<f1>": "help"
},
"insert": {
"<left>": "cursor-left",
"<right>": "cursor-right",
"<enter>": "send",
"<escape>": "mode-command",
"<backspace>": "backspace",
"C-8": "backspace",
"<delete>": "delete",
"<space>": "space"
},
"search": {
"<left>": "cursor-left",
"<right>": "cursor-right",
"<escape>": "clear-input",
"<enter>": "clear-input",
"<backspace>": "backspace",
"C-8": "backspace",
"<delete>": "delete",
"<space>": "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 |
|---------|-----------|----------------------------|

View File

@ -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
}

View File

@ -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
}

View File

@ -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: "",
},
},
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}()
}

12
main.go
View File

@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -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,
}
}

View File

@ -22,4 +22,4 @@ _testmain.go
*.exe
.idea/
*.iml
*.iml

View File

@ -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

View File

@ -4,5 +4,6 @@
# Please keep the list sorted.
Gary Burd <gary@beagledreams.com>
Google LLC (https://opensource.google.com/)
Joachim Bauch <mail@joachim-bauch.de>

View File

@ -51,7 +51,7 @@ subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn
<tr><td>Write message using io.WriteCloser</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextWriter">Yes</a></td><td>No, see note 3</td></tr>
</table>
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

View File

@ -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 == "" {

View File

@ -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)

15
vendor/github.com/gorilla/websocket/conn_write.go generated vendored Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
//

View File

@ -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)
}

View File

@ -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 {

77
vendor/github.com/gorilla/websocket/proxy.go generated vendored Normal file
View File

@ -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
}

View File

@ -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
// }
//

View File

@ -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

473
vendor/github.com/gorilla/websocket/x_net_proxy.go generated vendored Normal file
View File

@ -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
}

109
vendor/github.com/maruel/panicparse/stack/bucket.go generated vendored Normal file
View File

@ -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
}

View File

@ -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 "<unknown>"
}
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 "<unknown>"
}
// 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 "<unknown>", false

View File

@ -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| +)(\\?\\?|\\<autogenerated\\>|.+\\.(?:c|go|s))\\:(\\d+)(?:| \\+0x[0-9a-f]+)(?:| fp=0x[0-9a-f]+ sp=0x[0-9a-f]+)\n$")
reFile = regexp.MustCompile("^(?:\t| +)(\\?\\?|\\<autogenerated\\>|.+\\.(?: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) }

View File

@ -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},
}

View File

@ -1,2 +1,3 @@
*.test
*~
.idea/

View File

@ -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.

33
vendor/github.com/nlopes/slack/Gopkg.lock generated vendored Normal file
View File

@ -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

13
vendor/github.com/nlopes/slack/Gopkg.toml generated vendored Normal file
View File

@ -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

View File

@ -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

View File

@ -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"},
}

View File

@ -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"`

View File

@ -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!

View File

@ -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
}

View File

@ -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

View File

@ -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")

View File

@ -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
}

107
vendor/github.com/nlopes/slack/dialog.go generated vendored Normal file
View File

@ -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()
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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

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

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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

View File

@ -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")

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

33
vendor/github.com/nlopes/slack/webhooks.go generated vendored Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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{},
}

View File

@ -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"`
}

View File

@ -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)

View File

@ -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))

View File

@ -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")
}

24
vendor/github.com/pkg/errors/.gitignore generated vendored Normal file
View File

@ -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

11
vendor/github.com/pkg/errors/.travis.yml generated vendored Normal file
View File

@ -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 ./...

23
vendor/github.com/pkg/errors/LICENSE generated vendored Normal file
View File

@ -0,0 +1,23 @@
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
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.

52
vendor/github.com/pkg/errors/README.md generated vendored Normal file
View File

@ -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

32
vendor/github.com/pkg/errors/appveyor.yml generated vendored Normal file
View File

@ -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

269
vendor/github.com/pkg/errors/errors.go generated vendored Normal file
View File

@ -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
}

178
vendor/github.com/pkg/errors/stack.go generated vendored Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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() {