Use conversations api

Fixes #150
This commit is contained in:
erroneousboat 2018-09-16 09:50:51 +02:00
commit f959524a7a
59 changed files with 2385 additions and 864 deletions

54
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 = "8ac1139f0b5b26ddb4a5c05b727e7ce155cc37f4"
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 = "6b883214c48afc975f37de07cda7d2fbd9d235af028191ffc815755619c9cfc6"
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

@ -14,6 +14,7 @@ const (
IconChannel = "#"
IconGroup = "☰"
IconIM = "●"
IconMpIM = "●" // TODO
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,10 +139,10 @@ 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)
}
cells = termui.DTrimTxCls(cells, c.List.InnerWidth())
@ -196,8 +201,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 +235,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 +245,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 +260,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 +289,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 +303,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

@ -3,7 +3,9 @@ package handlers
import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/0xAX/notificator"
@ -11,6 +13,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,7 +118,7 @@ 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
@ -241,7 +244,7 @@ func actionSend(ctx *context.AppContext) {
// Send message
err := ctx.Service.SendMessage(
ctx.View.Channels.SelectedChannel,
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
message,
)
if err != nil {
@ -251,8 +254,11 @@ func actionSend(ctx *context.AppContext) {
}
// 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)
}
}
@ -304,7 +310,7 @@ func actionSearchMode(ctx *context.AppContext) {
func actionGetMessages(ctx *context.AppContext) {
msgs := ctx.Service.GetMessages(
ctx.Service.Channels[ctx.View.Channels.SelectedChannel],
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
ctx.View.Chat.GetMaxItems(),
)
@ -380,7 +386,7 @@ 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),
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
ctx.View.Chat.GetMaxItems(),
)
@ -389,12 +395,15 @@ func actionChangeChannel(ctx *context.AppContext) {
// 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)
@ -403,8 +412,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
@ -412,7 +420,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 {
@ -421,8 +429,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)
}
@ -491,20 +498,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)
}()
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"log"
"regexp"
"sort"
"strconv"
"strings"
"sync"
@ -16,18 +17,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 +69,168 @@ 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 {
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 {
log.Fatal(err) // FIXME
}
// 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 {
log.Fatal(err) // FIXME
}
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
}
chanItem.Type = components.ChannelTypeGroup
buckets[1][chn.ID] = &tempChan{
channelItem: chanItem,
slackChannel: chn,
}
}
if chn.IsMpIM {
chanItem.Type = components.ChannelTypeMpIM
buckets[2][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
}
// GetUserPresence will get the presence of a specific user
@ -257,82 +243,37 @@ 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]
func (s *SlackService) MarkAsRead(channelID string) {
if channel.Notification {
s.Channels[channelID].Notification = false
// TODO: does this work with other channel types? See old one below,
// test this
s.Client.SetChannelReadMark(
channelID, fmt.Sprintf("%f",
float64(time.Now().Unix())),
)
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
// 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())),
// )
// }
}
// SendMessage will send a message to a particular channel
func (s *SlackService) SendMessage(channelID int, message string) error {
func (s *SlackService) SendMessage(channelID string, message string) error {
// https://godoc.org/github.com/nlopes/slack#PostMessageParameters
postParams := slack.PostMessageParameters{
@ -342,7 +283,7 @@ func (s *SlackService) SendMessage(channelID int, message string) error {
}
// https://godoc.org/github.com/nlopes/slack#Client.PostMessage
_, _, err := s.Client.PostMessage(s.Channels[channelID].ID, message, postParams)
_, _, err := s.Client.PostMessage(channelID, message, postParams)
if err != nil {
return err
}
@ -352,33 +293,18 @@ func (s *SlackService) SendMessage(channelID int, message string) error {
// 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 {
// TODO: check other parameters
// 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 {
log.Fatal(err) // FIXME
}
// Construct the messages
@ -540,44 +466,6 @@ 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
@ -701,3 +589,15 @@ func (s *SlackService) CreateMessageFromAttachments(atts []slack.Attachment) []c
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,3 +1,10 @@
### 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.

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

@ -164,7 +164,7 @@ func (api *Client) SendMessageContext(ctx context.Context, channelID string, opt
return "", "", "", err
}
if err = post(ctx, api.httpclient, string(config.mode), config.values, &response, api.debug); err != nil {
if err = postSlackMethod(ctx, api.httpclient, string(config.mode), config.values, &response, api.debug); err != nil {
return "", "", "", err
}
@ -207,6 +207,7 @@ const (
chatPostMessage sendMode = "chat.postMessage"
chatDelete sendMode = "chat.delete"
chatPostEphemeral sendMode = "chat.postEphemeral"
chatMeMessage sendMode = "chat.meMessage"
)
type sendConfig struct {
@ -247,6 +248,14 @@ func MsgOptionPostEphemeral2(userID string) MsgOption {
}
}
// 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 {
@ -373,7 +382,11 @@ func MsgOptionCompose(options ...MsgOption) MsgOption {
func MsgOptionParse(b bool) MsgOption {
return func(c *sendConfig) error {
var v string
if b { v = "1" } else { v = "0" }
if b {
v = "1"
} else {
v = "0"
}
c.values.Set("parse", v)
return nil
}

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
}
@ -90,7 +92,7 @@ func (api *Client) GetUsersInConversationContext(ctx context.Context, params *Ge
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,7 +114,7 @@ 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
}
@ -132,7 +134,7 @@ 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
}
@ -156,7 +158,7 @@ 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
}
@ -180,7 +182,7 @@ 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
}
@ -204,7 +206,7 @@ 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
}
@ -228,7 +230,7 @@ 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
}
@ -249,7 +251,7 @@ 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
}
@ -274,7 +276,7 @@ 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
}
@ -337,6 +339,10 @@ func (api *Client) LeaveConversationContext(ctx context.Context, channelID strin
}
response, err := channelRequest(ctx, api.httpclient, "conversations.leave", values, api.debug)
if err != nil {
return false, err
}
return response.NotInChannel, err
}
@ -388,7 +394,7 @@ 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
}
@ -428,7 +434,7 @@ 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
}
@ -465,7 +471,7 @@ 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
}
@ -489,7 +495,7 @@ 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
}
@ -551,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
}

View File

@ -2,80 +2,80 @@ package slack
import (
"context"
"errors"
"encoding/json"
"errors"
)
type DialogTrigger struct {
TriggerId string `json:"trigger_id"` //Required. Must respond within 3 seconds.
Dialog Dialog `json:"dialog"` //Required.
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.
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 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".
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.
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.
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"`
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"`
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
@ -90,18 +90,18 @@ func (api *Client) OpenDialogContext(ctx context.Context, triggerId string, dial
}
resp := DialogTrigger{
TriggerId: triggerId,
Dialog: dialog,
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 {
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,7 +61,7 @@ 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
}
@ -120,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

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

View File

@ -69,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"`
@ -89,8 +89,8 @@ type Msg struct {
// slash commands and interactive messages
ResponseType string `json:"response_type,omitempty"`
ReplaceOriginal bool `json:"replace_original,omitempty"`
DeleteOriginal bool `json:"delete_original,omitempty"`
ReplaceOriginal bool `json:"replace_original"`
DeleteOriginal bool `json:"delete_original"`
}
// Icon is used for bot messages
@ -118,14 +118,16 @@ 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

View File

@ -171,7 +171,8 @@ func doPost(ctx context.Context, client HTTPRequester, req *http.Request, intf i
return parseResponseBody(resp.Body, intf, debug)
}
func postJson(ctx context.Context, client HTTPRequester, endpoint, token string, json []byte, 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 {
@ -182,6 +183,7 @@ func postJson(ctx context.Context, client HTTPRequester, endpoint, token string,
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)
@ -192,7 +194,8 @@ func postForm(ctx context.Context, client HTTPRequester, endpoint string, values
return doPost(ctx, client, req, intf, debug)
}
func post(ctx context.Context, client HTTPRequester, path string, values url.Values, intf interface{}, debug bool) error {
// 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)
}
@ -214,7 +217,7 @@ 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,
@ -227,3 +230,11 @@ 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

@ -34,7 +34,7 @@ func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemR
}
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
}
@ -63,7 +63,7 @@ func (api *Client) RemovePinContext(ctx context.Context, channel string, item It
}
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
}
@ -83,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

@ -155,7 +155,7 @@ func (api *Client) AddReactionContext(ctx context.Context, name string, item Ite
}
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
}
@ -189,7 +189,7 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item
}
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
}
@ -223,7 +223,7 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params
}
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 {
@ -256,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

@ -12,6 +12,15 @@ import (
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.
@ -29,7 +38,7 @@ 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, "", err
}
@ -54,7 +63,7 @@ func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) {
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) {
response := &infoResponseFull{}
err = post(ctx, 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, "", err
@ -83,6 +92,14 @@ func RTMOptionDialer(d *websocket.Dialer) RTMOption {
}
}
// RTMOptionPingInterval determines how often to deliver a ping message to slack.
func RTMOptionPingInterval(d time.Duration) RTMOption {
return func(rtm *RTM) {
rtm.pingInterval = d
rtm.resetDeadman()
}
}
// NewRTM returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol.
func (api *Client) NewRTM(options ...RTMOption) *RTM {
@ -90,11 +107,12 @@ func (api *Client) NewRTM(options ...RTMOption) *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),

View File

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

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

@ -58,7 +58,7 @@ func (api *Client) AddStarContext(ctx context.Context, channel string, item Item
}
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
}
@ -87,7 +87,7 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I
}
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
}
@ -115,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

@ -168,9 +168,9 @@ type TeamIdentity struct {
}
type userResponseFull struct {
Members []User `json:"members,omitempty"`
User `json:"user,omitempty"`
UserPresence
Members []User `json:"members,omitempty"`
User `json:"user,omitempty"`
UserPresence
SlackResponse
Metadata ResponseMetadata `json:"response_metadata"`
}
@ -547,7 +547,7 @@ func (api *Client) GetUserProfileContext(ctx context.Context, userID string, inc
}
resp := &getUserProfileResponse{}
err := post(ctx, api.httpclient, "users.profile.get", values, &resp, api.debug)
err := postSlackMethod(ctx, api.httpclient, "users.profile.get", values, &resp, api.debug)
if err != nil {
return nil, err
}

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

@ -20,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
@ -71,9 +72,14 @@ func (rtm *RTM) Disconnect() error {
// avoid RTM disconnect race conditions
rtm.mu.Lock()
defer rtm.mu.Unlock()
// this channel is always closed on disconnect. lets the ManagedConnection() function
// properly clean up.
close(rtm.disconnected)
// 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")
@ -83,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
@ -108,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

@ -26,36 +26,33 @@ import (
// The defined error events are located in websocket_internals.go.
func (rtm *RTM) ManageConnection() {
var (
err error
connectionCount int
info *Info
conn *websocket.Conn
err error
info *Info
conn *websocket.Conn
)
for {
// BEGIN SENSITIVE CODE, make sure lock is unlocked in this section.
rtm.mu.Lock()
connectionCount++
for connectionCount := 0; ; connectionCount++ {
// start trying to connect
// the returned err is already passed onto the IncomingEvents channel
if info, conn, err = rtm.connect(connectionCount, rtm.useRTMStart); err != nil {
// when the connection is unsuccessful its fatal, and we need to bail out.
rtm.Debugf("Failed to connect with RTM on try %d: %s", connectionCount, err)
rtm.mu.Unlock()
return
}
// 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.mu.Unlock()
// END SENSITIVE CODE
rtm.Debugf("RTM connection succeeded on try %d", connectionCount)
keepRunning := make(chan bool)
@ -64,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
@ -198,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 {
@ -207,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 {
@ -225,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:
}
}
}
}
@ -295,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())
@ -339,25 +343,31 @@ func (rtm *RTM) receiveIncomingEvent() error {
// 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.
@ -388,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

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

@ -33,12 +33,14 @@ func CreateView(config *config.Config, svc *service.SlackService) *View {
// Chat: fill the component
msgs := svc.GetMessages(
svc.GetSlackChannel(channels.SelectedChannel),
channels.ChannelItems[channels.SelectedChannel].ID,
chat.GetMaxItems(),
)
chat.SetMessages(msgs)
chat.SetBorderLabel(svc.Channels[channels.SelectedChannel].GetChannelName())
chat.SetBorderLabel(
channels.ChannelItems[channels.SelectedChannel].GetChannelName(),
)
// Debug: create the component
debug := components.CreateDebugComponent(input.Par.Height)