Merge branch 'v0.3.0'

* v0.3.0: (25 commits)
  Update readme
  Update readme and reset defaults
  Fix MarkAsRead
  Make search more robust
  Remove RankFind
  Start with improved search
  Implement theming for several components
  Start with theming functionality
  Start with adding colors for messages
  Fix notification for channels
  Fix input component overflow runewidth > 1
  Refactor to get service reference out of components
  Add support for double width runes
  Fix Chat component border labels
  Speed up Channels component
  Add profiling endpoints
  Add overflow functionality Input component
  Update Debug component
  Update Debug component
  Refactor to create more loose coupling
  ...
This commit is contained in:
erroneousboat 2017-12-22 14:23:42 +01:00
commit ceb13292ac
51 changed files with 2869 additions and 824 deletions

View File

@ -11,7 +11,7 @@ Installation
#### Binary installation
[Download](https://github.com/erroneousboat/slack-term/releases) a
compatible binary for your system. For convenience place `slack-term` in a
compatible binary for your system. For convenience, place `slack-term` in a
directory where you can access it from the command line. Usually this is
`/usr/local/bin`.
@ -41,48 +41,70 @@ Setup
{
"slack_token": "yourslacktokenhere",
// OPTIONAL: add the following to use light theme, default is dark
"theme": "light",
// OPTIONAL: set the width of the sidebar (between 1 and 11), default is 1
"sidebar_width": 3,
// OPTIONAL: define custom key mappings, defaults are:
"key_map": {
"command": {
"i": "mode-insert",
"k": "channel-up",
"j": "channel-down",
"g": "channel-top",
"G": "channel-bottom",
"i": "mode-insert",
"k": "channel-up",
"j": "channel-down",
"g": "channel-top",
"G": "channel-bottom",
"<previous>": "chat-up",
"C-b": "chat-up",
"C-u": "chat-up",
"<next>": "chat-down",
"C-f": "chat-down",
"C-d": "chat-down",
"q": "quit",
"<f1>": "help"
"C-b": "chat-up",
"C-u": "chat-up",
"<next>": "chat-down",
"C-f": "chat-down",
"C-d": "chat-down",
"n": "channel-search-next",
"N": "channel-search-previous",
"q": "quit",
"<f1>": "help"
},
"insert": {
"<left>": "cursor-left",
"<right>": "cursor-right",
"<enter>": "send",
"<escape>": "mode-command",
"<left>": "cursor-left",
"<right>": "cursor-right",
"<enter>": "send",
"<escape>": "mode-command",
"<backspace>": "backspace",
"C-8": "backspace",
"<delete>": "delete",
"<space>": "space"
"C-8": "backspace",
"<delete>": "delete",
"<space>": "space"
},
"search": {
"<left>": "cursor-left",
"<right>": "cursor-right",
"<escape>": "clear-input",
"<enter>": "clear-input",
"<left>": "cursor-left",
"<right>": "cursor-right",
"<escape>": "clear-input",
"<enter>": "clear-input",
"<backspace>": "backspace",
"C-8": "backspace",
"<delete>": "delete",
"<space>": "space"
"C-8": "backspace",
"<delete>": "delete",
"<space>": "space"
}
},
// OPTIONAL: override the default theme (see wiki for more information),
// defaults are:
"theme": {
"view": {
"fg": "white",
"bg": "default",
"border_fg": "white",
"border_bg": "white",
"par_fg": "white",
"par_label_fg": "white"
},
"channel": {
"prefix": "",
"icon": "",
"text": ""
},
"message": {
"time": "",
"name": "",
"text": ""
}
}
}
@ -126,6 +148,8 @@ in your `slack-term.json` file.
| command | `pg-down` | scroll chat pane down |
| command | `ctrl-f` | scroll chat pane down |
| command | `ctrl-d` | scroll chat pane down |
| command | `n` | next search match |
| command | `N` | previous search match |
| command | `q` | quit |
| command | `f1` | help |
| insert | `left` | move input cursor left |

View File

@ -2,11 +2,10 @@ package components
import (
"fmt"
"strings"
"html"
"github.com/erroneousboat/termui"
"github.com/erroneousboat/slack-term/service"
"github.com/renstrom/fuzzysearch/fuzzy"
)
const (
@ -15,22 +14,96 @@ const (
IconChannel = "#"
IconGroup = "☰"
IconIM = "●"
IconNotification = "🞷"
IconNotification = "*"
PresenceAway = "away"
PresenceActive = "active"
ChannelTypeChannel = "channel"
ChannelTypeGroup = "group"
ChannelTypeIM = "im"
)
type ChannelItem struct {
ID string
Name string
Topic string
Type string
UserID string
Presence string
Notification bool
StylePrefix string
StyleIcon string
StyleText string
}
// ToString will set the label of the channel, how it will be
// displayed on screen. Based on the type, different icons are
// shown, as well as an optional notification icon.
func (c ChannelItem) ToString() string {
var prefix string
if c.Notification {
prefix = IconNotification
} else {
prefix = " "
}
var icon string
switch c.Type {
case ChannelTypeChannel:
icon = IconChannel
case ChannelTypeGroup:
icon = IconGroup
case ChannelTypeIM:
switch c.Presence {
case PresenceActive:
icon = IconOnline
case PresenceAway:
icon = IconOffline
default:
icon = IconIM
}
}
label := fmt.Sprintf(
"[%s](%s) [%s](%s) [%s](%s)",
prefix, c.StylePrefix,
icon, c.StyleIcon,
c.Name, c.StyleText,
)
return label
}
// GetChannelName will return a formatted representation of the
// name of the channel
func (c ChannelItem) GetChannelName() string {
var channelName string
if c.Topic != "" {
channelName = fmt.Sprintf("%s - %s",
html.UnescapeString(c.Name),
html.UnescapeString(c.Topic),
)
} else {
channelName = c.Name
}
return channelName
}
// Channels is the definition of a Channels component
type Channels struct {
List *termui.List
SelectedChannel int // index of which channel is selected from the List
Offset int // from what offset are channels rendered
CursorPosition int // the y position of the 'cursor'
SearchMatches []int // index of the search matches
SearchPosition int // current position of a search match
}
// CreateChannels is the constructor for the Channels component
func CreateChannels(svc *service.SlackService, inputHeight int) *Channels {
func CreateChannelsComponent(inputHeight int) *Channels {
channels := &Channels{
List: termui.NewList(),
}
@ -42,9 +115,6 @@ func CreateChannels(svc *service.SlackService, inputHeight int) *Channels {
channels.Offset = 0
channels.CursorPosition = channels.List.InnerBounds().Min.Y
channels.GetChannels(svc)
channels.SetPresenceForIMChannels(svc)
return channels
}
@ -60,6 +130,7 @@ func (c *Channels) Buffer() termui.Buffer {
break
}
// Set the visible cursor
var cells []termui.Cell
if y == c.CursorPosition {
cells = termui.DefaultTxBuilder.Build(
@ -125,27 +196,8 @@ func (c *Channels) SetY(y int) {
c.List.SetY(y)
}
// GetChannels will get all available channels from the SlackService
func (c *Channels) GetChannels(svc *service.SlackService) {
for _, slackChan := range svc.GetChannels() {
label := setChannelLabel(slackChan, false)
c.List.Items = append(c.List.Items, label)
}
}
// SetPresenceForIMChannels this will set the correct icon for
// IM channels for when they're online of away
func (c *Channels) SetPresenceForIMChannels(svc *service.SlackService) {
for _, slackChan := range svc.GetChannels() {
if slackChan.Type == service.ChannelTypeIM {
presence, err := svc.GetUserPresence(slackChan.UserID)
if err != nil {
continue
}
c.SetPresence(svc, slackChan.UserID, presence)
}
}
func (c *Channels) SetChannels(channels []string) {
c.List.Items = channels
}
// SetSelectedChannel sets the SelectedChannel given the index
@ -153,12 +205,16 @@ 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 {
c.SetSelectedChannel(c.SelectedChannel - 1)
c.ScrollUp()
c.ClearNewMessageIndicator()
}
}
@ -167,7 +223,6 @@ func (c *Channels) MoveCursorDown() {
if c.SelectedChannel < len(c.List.Items)-1 {
c.SetSelectedChannel(c.SelectedChannel + 1)
c.ScrollDown()
c.ClearNewMessageIndicator()
}
}
@ -221,134 +276,71 @@ func (c *Channels) ScrollDown() {
// when a match has been found the selected channel will then
// be the channel that has been found
func (c *Channels) Search(term string) {
for i, item := range c.List.Items {
if strings.Contains(item, term) {
c.SearchMatches = make([]int, 0)
// The new position
newPos := i
matches := fuzzy.Find(term, c.List.Items)
// Is the new position in range of the current view?
minRange := c.Offset
maxRange := c.Offset + (c.List.InnerBounds().Max.Y - 2)
if newPos < minRange {
// newPos is above, we need to scroll up.
c.SetSelectedChannel(i)
// How much do we need to scroll to get it into range?
c.Offset = c.Offset - (minRange - newPos)
} else if newPos > maxRange {
// newPos is below, we need to scroll down
c.SetSelectedChannel(i)
// How much do we need to scroll to get it into range?
c.Offset = c.Offset + (newPos - maxRange)
} else {
// newPos is inside range
c.SetSelectedChannel(i)
for _, m := range matches {
for i, item := range c.List.Items {
if m == item {
c.SearchMatches = append(c.SearchMatches, i)
break
}
// Set cursor to correct position
c.CursorPosition = (newPos - c.Offset) + 1
break
}
}
}
// SetNotification will be called when a new message arrives and will
// render an notification icon in front of the channel name
func (c *Channels) SetNotification(svc *service.SlackService, channelID string) {
// Get the correct Channel from svc.Channels
var index int
for i, channel := range svc.Channels {
if channelID == channel.ID {
index = i
break
}
}
if !strings.Contains(c.List.Items[index], IconNotification) {
// The order of svc.Channels relates to the order of
// List.Items, index will be the index of the channel
c.List.Items[index] = fmt.Sprintf(
"%s %s", IconNotification, strings.TrimSpace(c.List.Items[index]),
)
if len(c.SearchMatches) > 0 {
c.GotoPosition(0)
c.SearchPosition = 0
}
// Play terminal bell sound
fmt.Print("\a")
}
// ClearNewMessageIndicator will remove the notification icon in front of
// a channel that received a new message. This will happen as one will
// move up or down the cursor for Channels
func (c *Channels) ClearNewMessageIndicator() {
channelName := strings.Split(
c.List.Items[c.SelectedChannel],
fmt.Sprintf("%s ", IconNotification),
)
// GotoPosition is used by the search functionality to automatically
// scroll to a specific location in the channels component
func (c *Channels) GotoPosition(position int) {
if len(channelName) > 1 {
c.List.Items[c.SelectedChannel] = fmt.Sprintf(" %s", channelName[1])
// The new position
newPos := c.SearchMatches[position]
// Is the new position in range of the current view?
minRange := c.Offset
maxRange := c.Offset + (c.List.InnerBounds().Max.Y - 2)
if newPos < minRange {
// newPos is above, we need to scroll up.
c.SetSelectedChannel(newPos)
// How much do we need to scroll to get it into range?
c.Offset = c.Offset - (minRange - newPos)
} else if newPos > maxRange {
// newPos is below, we need to scroll down
c.SetSelectedChannel(newPos)
// How much do we need to scroll to get it into range?
c.Offset = c.Offset + (newPos - maxRange)
} else {
c.List.Items[c.SelectedChannel] = channelName[0]
// newPos is inside range
c.SetSelectedChannel(newPos)
}
// Set cursor to correct position
c.CursorPosition = (newPos - c.Offset) + 1
}
// SearchNext allows us to cycle through the c.SearchMatches
func (c *Channels) SearchNext() {
newPosition := c.SearchPosition + 1
if newPosition <= len(c.SearchMatches)-1 {
c.GotoPosition(newPosition)
c.SearchPosition = newPosition
}
}
// SetReadMark will send the ReadMark event on the service
func (c *Channels) SetReadMark(svc *service.SlackService) {
svc.SetChannelReadMark(svc.SlackChannels[c.SelectedChannel])
}
// SetPresence will set the correct icon for a IM Channel
func (c *Channels) SetPresence(svc *service.SlackService, userID string, presence string) {
// Get the correct Channel from svc.Channels
var index int
for i, channel := range svc.Channels {
if userID == channel.UserID {
index = i
break
}
}
switch presence {
case PresenceActive:
c.List.Items[index] = strings.Replace(
c.List.Items[index], IconOffline, IconOnline, 1,
)
case PresenceAway:
c.List.Items[index] = strings.Replace(
c.List.Items[index], IconOnline, IconOffline, 1,
)
default:
c.List.Items[index] = strings.Replace(
c.List.Items[index], IconOnline, IconOffline, 1,
)
}
}
// setChannelLabel will set the label of the channel, meaning, how it
// is displayed on screen. Based on the type, different icons are
// shown, as well as an optional notification icon.
func setChannelLabel(channel service.Channel, notification bool) string {
var prefix string
if notification {
prefix = IconNotification
} else {
prefix = " "
}
var label string
switch channel.Type {
case service.ChannelTypeChannel:
label = fmt.Sprintf("%s %s %s", prefix, IconChannel, channel.Name)
case service.ChannelTypeGroup:
label = fmt.Sprintf("%s %s %s", prefix, IconGroup, channel.Name)
case service.ChannelTypeIM:
label = fmt.Sprintf("%s %s %s", prefix, IconIM, channel.Name)
}
return label
// SearchPrev allows us to cycle through the c.SearchMatches
func (c *Channels) SearchPrev() {
newPosition := c.SearchPosition - 1
if newPosition >= 0 {
c.GotoPosition(newPosition)
c.SearchPosition = newPosition
}
}

View File

@ -5,13 +5,44 @@ import (
"html"
"sort"
"strings"
"time"
"github.com/erroneousboat/termui"
"github.com/erroneousboat/slack-term/config"
"github.com/erroneousboat/slack-term/service"
)
type Message struct {
Time time.Time
Name string
Content string
StyleTime string
StyleName string
StyleText string
}
func (m Message) ToString() string {
if (m.Time != time.Time{} && m.Name != "") {
return html.UnescapeString(
fmt.Sprintf(
"[[%s]](%s) [<%s>](%s) [%s](%s)",
m.Time.Format("15:04"),
m.StyleTime,
m.Name,
m.StyleName,
m.Content,
m.StyleText,
),
)
} else {
return html.UnescapeString(
fmt.Sprintf("[%s](%s)", m.Content, m.StyleText),
)
}
}
// Chat is the definition of a Chat component
type Chat struct {
List *termui.List
@ -19,7 +50,7 @@ type Chat struct {
}
// CreateChat is the constructor for the Chat struct
func CreateChat(svc *service.SlackService, inputHeight int, selectedSlackChannel interface{}, selectedChannel service.Channel) *Chat {
func CreateChatComponent(inputHeight int) *Chat {
chat := &Chat{
List: termui.NewList(),
Offset: 0,
@ -28,9 +59,6 @@ func CreateChat(svc *service.SlackService, inputHeight int, selectedSlackChannel
chat.List.Height = termui.TermHeight() - inputHeight
chat.List.Overflow = "wrap"
chat.GetMessages(svc, selectedSlackChannel)
chat.SetBorderLabel(selectedChannel)
return chat
}
@ -153,15 +181,17 @@ func (c *Chat) SetY(y int) {
c.List.SetY(y)
}
// GetMessages will get an array of strings for a specific channel which will
// contain messages in turn all these messages will be added to List.Items
func (c *Chat) GetMessages(svc *service.SlackService, channel interface{}) {
// Get the count of message that fit in the pane
count := c.List.InnerBounds().Max.Y - c.List.InnerBounds().Min.Y
messages := svc.GetMessages(channel, count)
// GetMaxItems return the maximal amount of items can fit in the Chat
// component
func (c *Chat) GetMaxItems() int {
return c.List.InnerBounds().Max.Y - c.List.InnerBounds().Min.Y
}
for _, message := range messages {
c.AddMessage(message)
// SetMessages will put the provided messages into the Items field of the
// Chat view
func (c *Chat) SetMessages(messages []string) {
for _, msg := range messages {
c.List.Items = append(c.List.Items, html.UnescapeString(msg))
}
}
@ -208,16 +238,7 @@ func (c *Chat) ScrollDown() {
}
// SetBorderLabel will set Label of the Chat pane to the specified string
func (c *Chat) SetBorderLabel(channel service.Channel) {
var channelName string
if channel.Topic != "" {
channelName = fmt.Sprintf("%s - %s",
channel.Name,
channel.Topic,
)
} else {
channelName = channel.Name
}
func (c *Chat) SetBorderLabel(channelName string) {
c.List.BorderLabel = channelName
}

58
components/debug.go Normal file
View File

@ -0,0 +1,58 @@
package components
import "github.com/erroneousboat/termui"
// Debug can be used to relay debugging information in the Debug component,
// see event.go on how to use it
type Debug struct {
Par *termui.Par
List *termui.List
}
func CreateDebugComponent(inputHeight int) *Debug {
debug := &Debug{
List: termui.NewList(),
}
debug.List.BorderLabel = "Debug"
debug.List.Height = termui.TermHeight() - inputHeight
return debug
}
// Buffer implements interface termui.Bufferer
func (d *Debug) Buffer() termui.Buffer {
return d.List.Buffer()
}
// GetHeight implements interface termui.GridBufferer
func (d *Debug) GetHeight() int {
return d.List.Block.GetHeight()
}
// SetWidth implements interface termui.GridBufferer
func (d *Debug) SetWidth(w int) {
d.List.SetWidth(w)
}
// SetX implements interface termui.GridBufferer
func (d *Debug) SetX(x int) {
d.List.SetX(x)
}
// SetY implements interface termui.GridBufferer
func (d *Debug) SetY(y int) {
d.List.SetY(y)
}
// Println will add the text to the Debug component
func (d *Debug) Println(text string) {
d.List.Items = append(d.List.Items, text)
// When at the end remove first item
if len(d.List.Items) > d.List.InnerBounds().Max.Y-1 {
d.List.Items = d.List.Items[1:]
}
termui.Render(d)
}

View File

@ -2,23 +2,26 @@ package components
import (
"github.com/erroneousboat/termui"
"github.com/erroneousboat/slack-term/service"
runewidth "github.com/mattn/go-runewidth"
)
// Input is the definition of an Input component
type Input struct {
Par *termui.Par
Text []rune
CursorPosition int
Par *termui.Par
Text []rune
CursorPositionScreen int
CursorPositionText int
Offset int
}
// CreateInput is the constructor of the Input struct
func CreateInput() *Input {
func CreateInputComponent() *Input {
input := &Input{
Par: termui.NewPar(""),
Text: make([]rune, 0),
CursorPosition: 0,
Par: termui.NewPar(""),
Text: make([]rune, 0),
CursorPositionScreen: 0,
CursorPositionText: 0,
Offset: 0,
}
input.Par.Height = 3
@ -30,10 +33,11 @@ func CreateInput() *Input {
func (i *Input) Buffer() termui.Buffer {
buf := i.Par.Buffer()
// Set visible cursor
char := buf.At(i.Par.InnerX()+i.CursorPosition, i.Par.Block.InnerY())
// Set visible cursor, get char at screen cursor position
char := buf.At(i.Par.InnerX()+i.CursorPositionScreen, i.Par.Block.InnerY())
buf.Set(
i.Par.InnerX()+i.CursorPosition,
i.Par.InnerX()+i.CursorPositionScreen,
i.Par.Block.InnerY(),
termui.Cell{
Ch: char.Ch,
@ -65,57 +69,117 @@ func (i *Input) SetY(y int) {
i.Par.SetY(y)
}
// SendMessage send the input text through the SlackService
func (i *Input) SendMessage(svc *service.SlackService, channel string, message string) {
svc.SendMessage(channel, message)
}
// Insert will insert a given key at the place of the current CursorPosition
// Insert will insert a given key at the place of the current CursorPositionText
func (i *Input) Insert(key rune) {
if len(i.Text) < i.Par.InnerBounds().Dx()-1 {
// Append key to the left side
left := make([]rune, len(i.Text[0:i.CursorPositionText]))
copy(left, i.Text[0:i.CursorPositionText])
left = append(left, key)
left := make([]rune, len(i.Text[0:i.CursorPosition]))
copy(left, i.Text[0:i.CursorPosition])
left = append(left, key)
// Combine left and right side
i.Text = append(left, i.Text[i.CursorPositionText:]...)
i.Text = append(left, i.Text[i.CursorPosition:]...)
i.Par.Text = string(i.Text)
i.MoveCursorRight()
}
i.MoveCursorRight()
}
// Backspace will remove a character in front of the CursorPosition
// Backspace will remove a character in front of the CursorPositionText
func (i *Input) Backspace() {
if i.CursorPosition > 0 {
i.Text = append(i.Text[0:i.CursorPosition-1], i.Text[i.CursorPosition:]...)
i.Par.Text = string(i.Text)
if i.CursorPositionText > 0 {
i.MoveCursorLeft()
i.Text = append(i.Text[0:i.CursorPositionText], i.Text[i.CursorPositionText+1:]...)
i.Par.Text = string(i.Text[i.Offset:])
}
}
// Delete will remove a character at the CursorPosition
// Delete will remove a character at the CursorPositionText
func (i *Input) Delete() {
if i.CursorPosition < len(i.Text) {
i.Text = append(i.Text[0:i.CursorPosition], i.Text[i.CursorPosition+1:]...)
i.Par.Text = string(i.Text)
if i.CursorPositionText < len(i.Text) {
i.Text = append(i.Text[0:i.CursorPositionText], i.Text[i.CursorPositionText+1:]...)
i.Par.Text = string(i.Text[i.Offset:])
}
}
// MoveCursorRight will increase the current CursorPosition with 1
// MoveCursorRight will increase the current CursorPositionText with 1
func (i *Input) MoveCursorRight() {
if i.CursorPosition < len(i.Text) {
i.CursorPosition++
if i.CursorPositionText < len(i.Text) {
i.CursorPositionText++
i.ScrollRight()
}
i.Par.Text = string(i.Text[i.Offset:])
}
// MoveCursorLeft will decrease the current CursorPositionText with 1
func (i *Input) MoveCursorLeft() {
if i.CursorPositionText > 0 {
i.CursorPositionText--
i.ScrollLeft()
}
i.Par.Text = string(i.Text[i.Offset:])
}
func (i *Input) ScrollLeft() {
// Is the cursor at the far left of the Input component?
if i.CursorPositionScreen == 0 {
// Decrease offset to show what is on the left side
if i.Offset > 0 {
i.Offset--
}
} else {
i.CursorPositionScreen -= i.GetRuneWidthRight()
}
}
// MoveCursorLeft will decrease the current CursorPosition with 1
func (i *Input) MoveCursorLeft() {
if i.CursorPosition > 0 {
i.CursorPosition--
func (i *Input) ScrollRight() {
// Is the cursor at the far right of the Input component, cursor
// isn't at the end of the text
if (i.CursorPositionScreen + i.GetRuneWidthLeft()) > i.Par.InnerBounds().Dx()-1 {
// Increase offset to show what is on the right side
if i.Offset < len(i.Text) {
i.Offset = i.CalculateOffset()
i.CursorPositionScreen = i.GetRuneWidthOffsetToCursor()
}
} else {
i.CursorPositionScreen += i.GetRuneWidthLeft()
}
}
// CalculateOffset will, based on the width of the runes on the
// left of the text cursor, calculate the offset that needs to
// be used by the Inpute Component
func (i *Input) CalculateOffset() int {
var offset int
var currentRuneWidth int
for j := (i.CursorPositionText - 1); currentRuneWidth < i.GetMaxWidth()-1; j-- {
currentRuneWidth += runewidth.RuneWidth(i.Text[j])
offset = j
}
return offset
}
// GetRunWidthOffsetToCursor will get the rune width of all
// the runes from the offset until the text cursor
func (i *Input) GetRuneWidthOffsetToCursor() int {
return runewidth.StringWidth(string(i.Text[i.Offset:i.CursorPositionText]))
}
// GetRuneWidthLeft will get the width of a rune on the left side
// of the CursorPositionText
func (i *Input) GetRuneWidthLeft() int {
return runewidth.RuneWidth(i.Text[i.CursorPositionText-1])
}
// GetRuneWidthLeft will get the width of a rune on the right side
// of the CursorPositionText
func (i *Input) GetRuneWidthRight() int {
return runewidth.RuneWidth(i.Text[i.CursorPositionText])
}
// IsEmpty will return true when the input is empty
func (i *Input) IsEmpty() bool {
if i.Par.Text == "" {
@ -128,10 +192,17 @@ func (i *Input) IsEmpty() bool {
func (i *Input) Clear() {
i.Text = make([]rune, 0)
i.Par.Text = ""
i.CursorPosition = 0
i.CursorPositionScreen = 0
i.CursorPositionText = 0
}
// GetText returns the text currently in the input
func (i *Input) GetText() string {
return i.Par.Text
}
// GetMaxWidth returns the maximum number of positions
// the Input component can display
func (i *Input) GetMaxWidth() int {
return i.Par.InnerBounds().Dx() - 1
}

View File

@ -1,6 +1,14 @@
package components
import "github.com/erroneousboat/termui"
import (
"github.com/erroneousboat/termui"
)
const (
CommandMode = "NORMAL"
InsertMode = "INSERT"
SearchMode = "SEARCH"
)
// Mode is the definition of Mode component
type Mode struct {
@ -8,12 +16,13 @@ type Mode struct {
}
// CreateMode is the constructor of the Mode struct
func CreateMode() *Mode {
func CreateModeComponent() *Mode {
mode := &Mode{
Par: termui.NewPar("NORMAL"),
Par: termui.NewPar(CommandMode),
}
mode.Par.Height = 3
mode.SetCommandMode()
return mode
}
@ -80,3 +89,18 @@ func (m *Mode) SetX(x int) {
func (m *Mode) SetY(y int) {
m.Par.SetY(y)
}
func (m *Mode) SetInsertMode() {
m.Par.Text = InsertMode
termui.Render(m)
}
func (m *Mode) SetCommandMode() {
m.Par.Text = CommandMode
termui.Render(m)
}
func (m *Mode) SetSearchMode() {
m.Par.Text = SearchMode
termui.Render(m)
}

View File

@ -11,18 +11,51 @@ import (
// Config is the definition of a Config struct
type Config struct {
SlackToken string `json:"slack_token"`
Theme string `json:"theme"`
SidebarWidth int `json:"sidebar_width"`
MainWidth int `json:"-"`
KeyMap map[string]keyMapping `json:"key_map"`
Theme Theme `json:"theme"`
}
type keyMapping map[string]string
// NewConfig loads the config file and returns a Config struct
func NewConfig(filepath string) (*Config, error) {
cfg := Config{
Theme: "dark",
cfg := getDefaultConfig()
file, err := os.Open(filepath)
if err != nil {
return &cfg, err
}
if err := json.NewDecoder(file).Decode(&cfg); err != nil {
return &cfg, err
}
if cfg.SlackToken == "" {
return &cfg, errors.New("couldn't find 'slack_token' parameter")
}
if cfg.SidebarWidth < 1 || cfg.SidebarWidth > 11 {
return &cfg, errors.New("please specify the 'sidebar_width' between 1 and 11")
}
cfg.MainWidth = 12 - cfg.SidebarWidth
termui.ColorMap = map[string]termui.Attribute{
"fg": termui.StringToAttribute(cfg.Theme.View.Fg),
"bg": termui.StringToAttribute(cfg.Theme.View.Bg),
"border.fg": termui.StringToAttribute(cfg.Theme.View.BorderFg),
"label.fg": termui.StringToAttribute(cfg.Theme.View.LabelFg),
"par.fg": termui.StringToAttribute(cfg.Theme.View.ParFg),
"par.label.bg": termui.StringToAttribute(cfg.Theme.View.ParLabelFg),
}
return &cfg, nil
}
func getDefaultConfig() Config {
return Config{
SidebarWidth: 1,
MainWidth: 11,
KeyMap: map[string]keyMapping{
@ -39,6 +72,8 @@ func NewConfig(filepath string) (*Config, error) {
"<next>": "chat-down",
"C-f": "chat-down",
"C-d": "chat-down",
"n": "channel-search-next",
"N": "channel-search-prev",
"q": "quit",
"<f1>": "help",
},
@ -63,37 +98,25 @@ func NewConfig(filepath string) (*Config, error) {
"<space>": "space",
},
},
Theme: Theme{
View: View{
Fg: "white",
Bg: "default",
BorderFg: "white",
LabelFg: "green,bold",
ParFg: "white",
ParLabelFg: "white",
},
Channel: Channel{
Prefix: "",
Icon: "",
Text: "",
},
Message: Message{
Time: "",
Name: "",
Text: "",
},
},
}
file, err := os.Open(filepath)
if err != nil {
return &cfg, err
}
if err := json.NewDecoder(file).Decode(&cfg); err != nil {
return &cfg, err
}
if cfg.SlackToken == "" {
return &cfg, errors.New("couldn't find 'slack_token' parameter")
}
if cfg.SidebarWidth < 1 || cfg.SidebarWidth > 11 {
return &cfg, errors.New("please specify the 'sidebar_width' between 1 and 11")
}
cfg.MainWidth = 12 - cfg.SidebarWidth
if cfg.Theme == "light" {
termui.ColorMap = map[string]termui.Attribute{
"fg": termui.ColorBlack,
"bg": termui.ColorWhite,
"border.fg": termui.ColorBlack,
"label.fg": termui.ColorBlue,
"par.fg": termui.ColorYellow,
"par.label.bg": termui.ColorWhite,
}
}
return &cfg, nil
}

28
config/theme.go Normal file
View File

@ -0,0 +1,28 @@
package config
type Theme struct {
View View `json:"view"`
Channel Channel `json:"channel"`
Message Message `json:"message"`
}
type View struct {
Fg string `json:"fg"`
Bg string `json:"bg"`
BorderFg string `json:"border_fg"`
LabelFg string `json:"border_fg"`
ParFg string `json:"par_fg"`
ParLabelFg string `json:"par_label_fg"`
}
type Message struct {
Time string `json:"time"`
Name string `json:"name"`
Text string `json:"text"`
}
type Channel struct {
Prefix string `json:"prefix"`
Icon string `json:"icon"`
Text string `json:"text"`
}

View File

@ -1,6 +1,9 @@
package context
import (
"net/http"
_ "net/http/pprof"
"github.com/erroneousboat/termui"
termbox "github.com/nsf/termbox-go"
@ -21,12 +24,19 @@ type AppContext struct {
Body *termui.Grid
View *views.View
Config *config.Config
Debug bool
Mode string
}
// CreateAppContext creates an application context which can be passed
// and referenced througout the application
func CreateAppContext(flgConfig string) (*AppContext, error) {
func CreateAppContext(flgConfig string, flgDebug bool) (*AppContext, error) {
if flgDebug {
go func() {
http.ListenAndServe(":6060", nil)
}()
}
// Load config
config, err := config.NewConfig(flgConfig)
if err != nil {
@ -34,16 +44,50 @@ func CreateAppContext(flgConfig string) (*AppContext, error) {
}
// Create Service
svc := service.NewSlackService(config.SlackToken)
svc, err := service.NewSlackService(config)
if err != nil {
return nil, err
}
// Create ChatView
view := views.CreateChatView(svc)
// Create the main view
view := views.CreateView(config, svc)
// Setup the interface
if flgDebug {
termui.Body.AddRows(
termui.NewRow(
termui.NewCol(config.SidebarWidth, 0, view.Channels),
termui.NewCol(config.MainWidth-5, 0, view.Chat),
termui.NewCol(config.MainWidth-6, 0, view.Debug),
),
termui.NewRow(
termui.NewCol(config.SidebarWidth, 0, view.Mode),
termui.NewCol(config.MainWidth, 0, view.Input),
),
)
} else {
termui.Body.AddRows(
termui.NewRow(
termui.NewCol(config.SidebarWidth, 0, view.Channels),
termui.NewCol(config.MainWidth, 0, view.Chat),
),
termui.NewRow(
termui.NewCol(config.SidebarWidth, 0, view.Mode),
termui.NewCol(config.MainWidth, 0, view.Input),
),
)
}
termui.Body.Align()
termui.Render(termui.Body)
return &AppContext{
EventQueue: make(chan termbox.Event, 20),
Service: svc,
Body: termui.Body,
View: view,
Config: config,
Debug: flgDebug,
Mode: CommandMode,
}, nil
}

View File

@ -1,6 +1,7 @@
package handlers
import (
"fmt"
"os"
"strconv"
"time"
@ -19,24 +20,26 @@ var timer *time.Timer
// these action names can then be used to bind them to specific keys
// in the Config.
var actionMap = map[string]func(*context.AppContext){
"space": actionSpace,
"backspace": actionBackSpace,
"delete": actionDelete,
"cursor-right": actionMoveCursorRight,
"cursor-left": actionMoveCursorLeft,
"send": actionSend,
"quit": actionQuit,
"mode-insert": actionInsertMode,
"mode-command": actionCommandMode,
"mode-search": actionSearchMode,
"clear-input": actionClearInput,
"channel-up": actionMoveCursorUpChannels,
"channel-down": actionMoveCursorDownChannels,
"channel-top": actionMoveCursorTopChannels,
"channel-bottom": actionMoveCursorBottomChannels,
"chat-up": actionScrollUpChat,
"chat-down": actionScrollDownChat,
"help": actionHelp,
"space": actionSpace,
"backspace": actionBackSpace,
"delete": actionDelete,
"cursor-right": actionMoveCursorRight,
"cursor-left": actionMoveCursorLeft,
"send": actionSend,
"quit": actionQuit,
"mode-insert": actionInsertMode,
"mode-command": actionCommandMode,
"mode-search": actionSearchMode,
"clear-input": actionClearInput,
"channel-up": actionMoveCursorUpChannels,
"channel-down": actionMoveCursorDownChannels,
"channel-top": actionMoveCursorTopChannels,
"channel-bottom": actionMoveCursorBottomChannels,
"channel-search-next": actionSearchNextChannels,
"channel-search-prev": actionSearchPrevChannels,
"chat-up": actionScrollUpChat,
"chat-down": actionScrollDownChat,
"help": actionHelp,
}
func RegisterEventHandlers(ctx *context.AppContext) {
@ -56,6 +59,13 @@ func eventHandler(ctx *context.AppContext) {
ev := <-ctx.EventQueue
handleTermboxEvents(ctx, ev)
handleMoreTermboxEvents(ctx, ev)
// Place your debugging statements here
if ctx.Debug {
ctx.View.Debug.Println(
"event received",
)
}
}
}()
}
@ -101,7 +111,9 @@ func messageHandler(ctx *context.AppContext) {
// reverse order of messages, mainly done
// when attachments are added to message
for i := len(msg) - 1; i >= 0; i-- {
ctx.View.Chat.AddMessage(msg[i])
ctx.View.Chat.AddMessage(
msg[i].ToString(),
)
}
termui.Render(ctx.View.Chat)
@ -202,9 +214,8 @@ func actionSend(ctx *context.AppContext) {
ctx.View.Input.Clear()
ctx.View.Refresh()
ctx.View.Input.SendMessage(
ctx.Service,
ctx.Service.Channels[ctx.View.Channels.SelectedChannel].ID,
ctx.Service.SendMessage(
ctx.View.Channels.SelectedChannel,
message,
)
}
@ -238,31 +249,38 @@ func actionQuit(ctx *context.AppContext) {
func actionInsertMode(ctx *context.AppContext) {
ctx.Mode = context.InsertMode
ctx.View.Mode.Par.Text = "INSERT"
termui.Render(ctx.View.Mode)
ctx.View.Mode.SetInsertMode()
}
func actionCommandMode(ctx *context.AppContext) {
ctx.Mode = context.CommandMode
ctx.View.Mode.Par.Text = "NORMAL"
termui.Render(ctx.View.Mode)
ctx.View.Mode.SetCommandMode()
}
func actionSearchMode(ctx *context.AppContext) {
ctx.Mode = context.SearchMode
ctx.View.Mode.Par.Text = "SEARCH"
termui.Render(ctx.View.Mode)
ctx.View.Mode.SetSearchMode()
}
func actionGetMessages(ctx *context.AppContext) {
ctx.View.Chat.GetMessages(
ctx.Service,
msgs := ctx.Service.GetMessages(
ctx.Service.Channels[ctx.View.Channels.SelectedChannel],
ctx.View.Chat.GetMaxItems(),
)
var strMsgs []string
for _, msg := range msgs {
strMsgs = append(strMsgs, msg.ToString())
}
ctx.View.Chat.SetMessages(strMsgs)
termui.Render(ctx.View.Chat)
}
// actionMoveCursorUpChannels will execute the actionChangeChannel
// function. A time is implemented to support fast scrolling through
// the list without executing the actionChangeChannel event
func actionMoveCursorUpChannels(ctx *context.AppContext) {
go func() {
if timer != nil {
@ -275,10 +293,14 @@ func actionMoveCursorUpChannels(ctx *context.AppContext) {
timer = time.NewTimer(time.Second / 4)
<-timer.C
// Only actually change channel when the timer expires
actionChangeChannel(ctx)
}()
}
// actionMoveCursorDownChannels will execute the actionChangeChannel
// function. A time is implemented to support fast scrolling through
// the list without executing the actionChangeChannel event
func actionMoveCursorDownChannels(ctx *context.AppContext) {
go func() {
if timer != nil {
@ -291,6 +313,7 @@ func actionMoveCursorDownChannels(ctx *context.AppContext) {
timer = time.NewTimer(time.Second / 4)
<-timer.C
// Only actually change channel when the timer expires
actionChangeChannel(ctx)
}()
}
@ -305,35 +328,58 @@ func actionMoveCursorBottomChannels(ctx *context.AppContext) {
actionChangeChannel(ctx)
}
func actionSearchNextChannels(ctx *context.AppContext) {
ctx.View.Channels.SearchNext()
actionChangeChannel(ctx)
}
func actionSearchPrevChannels(ctx *context.AppContext) {
ctx.View.Channels.SearchPrev()
actionChangeChannel(ctx)
}
func actionChangeChannel(ctx *context.AppContext) {
// Clear messages from Chat pane
ctx.View.Chat.ClearMessages()
// Get message for the new channel
ctx.View.Chat.GetMessages(
ctx.Service,
ctx.Service.SlackChannels[ctx.View.Channels.SelectedChannel],
// 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.Chat.GetMaxItems(),
)
var strMsgs []string
for _, msg := range msgs {
strMsgs = append(strMsgs, msg.ToString())
}
// Set messages for the channel
ctx.View.Chat.SetMessages(strMsgs)
// Set channel name for the Chat pane
ctx.View.Chat.SetBorderLabel(
ctx.Service.Channels[ctx.View.Channels.SelectedChannel],
ctx.Service.Channels[ctx.View.Channels.SelectedChannel].GetChannelName(),
)
// Set read mark
ctx.View.Channels.SetReadMark(ctx.Service)
// Clear notification icon if there is any
ctx.Service.MarkAsRead(ctx.View.Channels.SelectedChannel)
ctx.View.Channels.SetChannels(ctx.Service.ChannelsToString())
termui.Render(ctx.View.Channels)
termui.Render(ctx.View.Chat)
}
func actionNewMessage(ctx *context.AppContext, channelID string) {
ctx.View.Channels.SetNotification(ctx.Service, channelID)
ctx.Service.MarkAsUnread(channelID)
ctx.View.Channels.SetChannels(ctx.Service.ChannelsToString())
termui.Render(ctx.View.Channels)
fmt.Print("\a")
}
func actionSetPresence(ctx *context.AppContext, channelID string, presence string) {
ctx.View.Channels.SetPresence(ctx.Service, channelID, presence)
ctx.Service.SetPresenceChannelEvent(channelID, presence)
ctx.View.Channels.SetChannels(ctx.Service.ChannelsToString())
termui.Render(ctx.View.Channels)
}

29
main.go
View File

@ -16,7 +16,7 @@ import (
)
const (
VERSION = "v0.2.3"
VERSION = "v0.3.0"
USAGE = `NAME:
slack-term - slack client for your terminal
@ -33,6 +33,7 @@ GLOBAL OPTIONS:
var (
flgConfig string
flgDebug bool
flgUsage bool
)
@ -51,6 +52,13 @@ func init() {
"location of config file",
)
flag.BoolVar(
&flgDebug,
"debug",
false,
"turn on debugging",
)
flag.Usage = func() {
fmt.Printf(USAGE, VERSION)
}
@ -76,30 +84,13 @@ func main() {
termui.DefaultEvtStream = customEvtStream
// Create context
ctx, err := context.CreateAppContext(flgConfig)
ctx, err := context.CreateAppContext(flgConfig, flgDebug)
if err != nil {
termbox.Close()
log.Println(err)
os.Exit(0)
}
// Setup body
termui.Body.AddRows(
termui.NewRow(
termui.NewCol(ctx.Config.SidebarWidth, 0, ctx.View.Channels),
termui.NewCol(ctx.Config.MainWidth, 0, ctx.View.Chat),
),
termui.NewRow(
termui.NewCol(ctx.Config.SidebarWidth, 0, ctx.View.Mode),
termui.NewCol(ctx.Config.MainWidth, 0, ctx.View.Input),
),
)
termui.Body.Align()
termui.Render(termui.Body)
// Set body in context
ctx.Body = termui.Body
// Register handlers
handlers.RegisterEventHandlers(ctx)

View File

@ -1,6 +1,7 @@
package service
import (
"errors"
"fmt"
"log"
"regexp"
@ -10,6 +11,7 @@ import (
"github.com/nlopes/slack"
"github.com/erroneousboat/slack-term/components"
"github.com/erroneousboat/slack-term/config"
)
@ -20,27 +22,22 @@ const (
)
type SlackService struct {
Client *slack.Client
RTM *slack.RTM
SlackChannels []interface{}
Channels []Channel
UserCache map[string]string
CurrentUserID string
}
type Channel struct {
ID string
Name string
Topic string
Type string
UserID string
Config *config.Config
Client *slack.Client
RTM *slack.RTM
SlackChannels []interface{}
Channels []components.ChannelItem
UserCache map[string]string
CurrentUserID string
CurrentUsername string
}
// NewSlackService is the constructor for the SlackService and will initialize
// the RTM and a Client
func NewSlackService(token string) *SlackService {
func NewSlackService(config *config.Config) (*SlackService, error) {
svc := &SlackService{
Client: slack.New(token),
Config: config,
Client: slack.New(config.SlackToken),
UserCache: make(map[string]string),
}
@ -49,7 +46,7 @@ func NewSlackService(token string) *SlackService {
// arrives
authTest, err := svc.Client.AuthTest()
if err != nil {
log.Fatal("ERROR: not able to authorize client, check your connection and/or slack-token")
return nil, errors.New("not able to authorize client, check your connection and or slack-token")
}
svc.CurrentUserID = authTest.UserID
@ -67,48 +64,64 @@ func NewSlackService(token string) *SlackService {
}
}
return svc
// Get name of current user
currentUser, err := svc.Client.GetUserInfo(svc.CurrentUserID)
if err != nil {
svc.CurrentUsername = "slack-term"
}
svc.CurrentUsername = currentUser.Name
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() []Channel {
var chans []Channel
func (s *SlackService) GetChannels() []string {
var chans []components.ChannelItem
// Channel
slackChans, err := s.Client.GetChannels(true)
if err != nil {
chans = append(chans, Channel{})
chans = append(chans, components.ChannelItem{})
}
for _, chn := range slackChans {
s.SlackChannels = append(s.SlackChannels, chn)
chans = append(
chans, Channel{
ID: chn.ID,
Name: chn.Name,
Topic: chn.Topic.Value,
Type: ChannelTypeChannel,
UserID: "",
},
)
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,
},
)
}
}
// Groups
slackGroups, err := s.Client.GetGroups(true)
if err != nil {
chans = append(chans, Channel{})
chans = append(chans, components.ChannelItem{})
}
for _, grp := range slackGroups {
s.SlackChannels = append(s.SlackChannels, grp)
chans = append(
chans, Channel{
ID: grp.ID,
Name: grp.Name,
Topic: grp.Topic.Value,
Type: ChannelTypeGroup,
UserID: "",
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,
},
)
}
@ -116,10 +129,13 @@ func (s *SlackService) GetChannels() []Channel {
// IM
slackIM, err := s.Client.GetIMChannels()
if err != nil {
chans = append(chans, Channel{})
chans = append(chans, components.ChannelItem{})
}
for _, im := range slackIM {
// FIXME: err
presence, _ := s.GetUserPresence(im.User)
// Uncover name, when we can't uncover name for
// IM channel this is then probably a deleted
// user, because we won't add deleted users
@ -129,12 +145,16 @@ func (s *SlackService) GetChannels() []Channel {
if ok {
chans = append(
chans,
Channel{
ID: im.ID,
Name: name,
Topic: "",
Type: ChannelTypeIM,
UserID: im.User,
components.ChannelItem{
ID: im.ID,
Name: name,
Topic: "",
Type: components.ChannelTypeIM,
UserID: im.User,
Presence: 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)
@ -143,7 +163,38 @@ func (s *SlackService) GetChannels() []Channel {
s.Channels = chans
return chans
var channels []string
for _, chn := range s.Channels {
channels = append(channels, chn.ToString())
}
return channels
}
// 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
}
// 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
}
}
s.Channels[index].Presence = presence
}
// GetSlackChannel returns the representation of a slack channel
func (s *SlackService) GetSlackChannel(selectedChannel int) interface{} {
return s.SlackChannels[selectedChannel]
}
// GetUserPresence will get the presence of a specific user
@ -178,20 +229,61 @@ func (s *SlackService) SetChannelReadMark(channel interface{}) {
}
}
// MarkAsRead will set the channel as read
func (s *SlackService) MarkAsRead(channelID int) {
channel := s.Channels[channelID]
if channel.Notification {
s.Channels[channelID].Notification = false
switch channel.Type {
case ChannelTypeChannel:
s.Client.SetChannelReadMark(
channel.ID, fmt.Sprintf("%f",
float64(time.Now().Unix())),
)
case ChannelTypeGroup:
s.Client.SetGroupReadMark(
channel.ID, fmt.Sprintf("%f",
float64(time.Now().Unix())),
)
case ChannelTypeIM:
s.Client.MarkIMChannel(
channel.ID, fmt.Sprintf("%f",
float64(time.Now().Unix())),
)
}
}
}
// MarkAsUnread will set the channel as unread
func (s *SlackService) MarkAsUnread(channelID string) {
var index int
for i, channel := range s.Channels {
if channel.ID == channelID {
index = i
break
}
}
s.Channels[index].Notification = true
}
// SendMessage will send a message to a particular channel
func (s *SlackService) SendMessage(channel string, message string) {
func (s *SlackService) SendMessage(channelID int, message string) {
// https://godoc.org/github.com/nlopes/slack#PostMessageParameters
postParams := slack.PostMessageParameters{
AsUser: true,
AsUser: true,
Username: s.CurrentUsername,
}
// https://godoc.org/github.com/nlopes/slack#Client.PostMessage
s.Client.PostMessage(channel, message, postParams)
s.Client.PostMessage(s.Channels[channelID].ID, message, postParams)
}
// GetMessages will get messages for a channel, group or im channel delimited
// by a count.
func (s *SlackService) GetMessages(channel interface{}, count int) []string {
func (s *SlackService) GetMessages(channel interface{}, count int) []components.Message {
// https://api.slack.com/methods/channels.history
historyParams := slack.HistoryParameters{
Count: count,
@ -221,7 +313,7 @@ func (s *SlackService) GetMessages(channel interface{}, count int) []string {
}
// Construct the messages
var messages []string
var messages []components.Message
for _, message := range history.Messages {
msg := s.CreateMessage(message)
messages = append(messages, msg...)
@ -229,7 +321,7 @@ func (s *SlackService) GetMessages(channel interface{}, count int) []string {
// Reverse the order of the messages, we want the newest in
// the last place
var messagesReversed []string
var messagesReversed []components.Message
for i := len(messages) - 1; i >= 0; i-- {
messagesReversed = append(messagesReversed, messages[i])
}
@ -244,8 +336,8 @@ func (s *SlackService) GetMessages(channel interface{}, count int) []string {
//
// This returns an array of string because we will try to uncover attachments
// associated with messages.
func (s *SlackService) CreateMessage(message slack.Message) []string {
var msgs []string
func (s *SlackService) CreateMessage(message slack.Message) []components.Message {
var msgs []components.Message
var name string
// Get username from cache
@ -280,7 +372,7 @@ func (s *SlackService) CreateMessage(message slack.Message) []string {
// When there are attachments append them
if len(message.Attachments) > 0 {
msgs = append(msgs, createMessageFromAttachments(message.Attachments)...)
msgs = append(msgs, s.CreateMessageFromAttachments(message.Attachments)...)
}
// Parse time
@ -291,21 +383,23 @@ func (s *SlackService) CreateMessage(message slack.Message) []string {
intTime := int64(floatTime)
// Format message
msg := fmt.Sprintf(
"[%s] <%s> %s",
time.Unix(intTime, 0).Format("15:04"),
name,
parseMessage(s, message.Text),
)
msg := components.Message{
Time: time.Unix(intTime, 0),
Name: name,
Content: parseMessage(s, message.Text),
StyleTime: s.Config.Theme.Message.Time,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
}
msgs = append(msgs, msg)
return msgs
}
func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent) []string {
func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent) []components.Message {
var msgs []string
var msgs []components.Message
var name string
// Append (edited) when an edited message is received
@ -346,7 +440,7 @@ func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent
// When there are attachments append them
if len(message.Attachments) > 0 {
msgs = append(msgs, createMessageFromAttachments(message.Attachments)...)
msgs = append(msgs, s.CreateMessageFromAttachments(message.Attachments)...)
}
// Parse time
@ -357,12 +451,14 @@ func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent
intTime := int64(floatTime)
// Format message
msg := fmt.Sprintf(
"[%s] <%s> %s",
time.Unix(intTime, 0).Format("15:04"),
name,
parseMessage(s, message.Text),
)
msg := components.Message{
Time: time.Unix(intTime, 0),
Name: name,
Content: parseMessage(s, message.Text),
StyleTime: s.Config.Theme.Message.Time,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
}
msgs = append(msgs, msg)
@ -391,13 +487,14 @@ func parseMessage(s *SlackService, msg string) string {
// <@U12345>
func parseMentions(s *SlackService, msg string) string {
r := regexp.MustCompile(`\<@(\w+\|*\w+)\>`)
rs := r.FindStringSubmatch(msg)
if len(rs) < 1 {
return msg
}
return r.ReplaceAllStringFunc(
msg, func(str string) string {
rs := r.FindStringSubmatch(str)
if len(rs) < 1 {
return str
}
var userID string
split := strings.Split(rs[1], "|")
if len(split) > 0 {
@ -443,27 +540,37 @@ func parseEmoji(msg string) string {
)
}
// createMessageFromAttachments will construct a array of string of the Field
// CreateMessageFromAttachments will construct a array of string of the Field
// values of Attachments from a Message.
func createMessageFromAttachments(atts []slack.Attachment) []string {
var msgs []string
func (s *SlackService) CreateMessageFromAttachments(atts []slack.Attachment) []components.Message {
var msgs []components.Message
for _, att := range atts {
for i := len(att.Fields) - 1; i >= 0; i-- {
msgs = append(msgs,
fmt.Sprintf(
msgs = append(msgs, components.Message{
Content: fmt.Sprintf(
"%s %s",
att.Fields[i].Title,
att.Fields[i].Value,
),
StyleTime: s.Config.Theme.Message.Time,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
},
)
}
if att.Text != "" {
msgs = append(msgs, att.Text)
msgs = append(
msgs,
components.Message{Content: fmt.Sprintf("%s", att.Text)},
)
}
if att.Title != "" {
msgs = append(msgs, att.Title)
msgs = append(
msgs,
components.Message{Content: fmt.Sprintf("%s", att.Title)},
)
}
}

View File

@ -53,7 +53,7 @@ func (c *cache) augmentGoroutine(goroutine *Goroutine) {
}
// 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)
@ -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

@ -35,7 +35,7 @@ var (
// - found next stack barrier at 0x123; expected
// - runtime: unexpected return pc for FUNC_NAME called from 0x123
reRoutineHeader = regexp.MustCompile("^goroutine (\\d+) \\[([^\\]]+)\\]\\:\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,12 +54,12 @@ var (
// when a signal is not correctly handled. It is printed with m.throwing>0.
// These are discarded.
// - For cgo, the source file may be "??".
reFile = regexp.MustCompile("^(?:\t| +)(\\?\\?|\\<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$")
reCreated = regexp.MustCompile("^created by (.+)\r?\n$")
reFunc = regexp.MustCompile("^(.+)\\((.*)\\)\r?\n$")
reElided = regexp.MustCompile("^\\.\\.\\.additional frames elided\\.\\.\\.\r?\n$")
// Include frequent GOROOT value on Windows, distro provided and user
// installed path. This simplifies the user's life when processing a trace
// generated on another VM.
@ -656,7 +656,7 @@ func ParseDump(r io.Reader, out io.Writer) ([]Goroutine, error) {
firstLine := false
for scanner.Scan() {
line := scanner.Text()
if line == "\n" {
if line == "\n" || line == "\r\n" {
if goroutine != nil {
goroutine = nil
continue

View File

@ -55,6 +55,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},
}

12
vendor/github.com/nlopes/slack/CHANGELOG.md generated vendored Normal file
View File

@ -0,0 +1,12 @@
### v0.1.0 - May 28, 2017
This is released before adding context support.
As the used context package is the one from Go 1.7 this will be the last
compatible with Go < 1.7.
Please check [0.1.0](https://github.com/nlopes/slack/releases/tag/v0.1.0)
### v0.0.1 - Jul 26, 2015
If you just updated from master and it broke your implementation, please
check [0.0.1](https://github.com/nlopes/slack/releases/tag/v0.0.1)

View File

@ -5,14 +5,25 @@ This library supports most if not all of the `api.slack.com` REST
calls, as well as the Real-Time Messaging protocol over websocket, in
a fully managed way.
## Change log
Note: If you just updated from master and it broke your implementation, please check [0.0.1](https://github.com/nlopes/slack/releases/tag/v0.0.1)
### v0.1.0 - May 28, 2017
This is released before adding context support.
As the used context package is the one from Go 1.7 this will be the last
compatible with Go < 1.7.
Please check [0.1.0](https://github.com/nlopes/slack/releases/tag/v0.1.0)
### CHANGELOG.md
As of this version a [CHANGELOG.md](https://github.com/nlopes/slack/blob/master/CHANGELOG.md) is available. Please visit it for updates.
## Installing
### *go get*
$ go get github.com/nlopes/slack
$ go get -u github.com/nlopes/slack
## Example

View File

@ -1,6 +1,7 @@
package slack
import (
"context"
"errors"
"fmt"
"net/url"
@ -11,9 +12,9 @@ type adminResponse struct {
Error string `json:"error"`
}
func adminRequest(method string, teamName string, values url.Values, debug bool) (*adminResponse, error) {
func adminRequest(ctx context.Context, method string, teamName string, values url.Values, debug bool) (*adminResponse, error) {
adminResponse := &adminResponse{}
err := parseAdminResponse(method, teamName, values, adminResponse, debug)
err := parseAdminResponse(ctx, method, teamName, values, adminResponse, debug)
if err != nil {
return nil, err
}
@ -27,6 +28,11 @@ func adminRequest(method string, teamName string, values url.Values, debug bool)
// DisableUser disabled a user account, given a user ID
func (api *Client) DisableUser(teamName string, uid string) error {
return api.DisableUserContext(context.Background(), teamName, uid)
}
// DisableUserContext disabled a user account, given a user ID with a custom context
func (api *Client) DisableUserContext(ctx context.Context, teamName string, uid string) error {
values := url.Values{
"user": {uid},
"token": {api.config.token},
@ -34,7 +40,7 @@ func (api *Client) DisableUser(teamName string, uid string) error {
"_attempts": {"1"},
}
_, err := adminRequest("setInactive", teamName, values, api.debug)
_, err := adminRequest(ctx, "setInactive", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err)
}
@ -43,13 +49,12 @@ func (api *Client) DisableUser(teamName string, uid string) error {
}
// InviteGuest invites a user to Slack as a single-channel guest
func (api *Client) InviteGuest(
teamName string,
channel string,
firstName string,
lastName string,
emailAddress string,
) error {
func (api *Client) InviteGuest(teamName, channel, firstName, lastName, emailAddress string) error {
return api.InviteGuestContext(context.Background(), teamName, channel, firstName, lastName, emailAddress)
}
// InviteGuestContext invites a user to Slack as a single-channel guest with a custom context
func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, firstName, lastName, emailAddress string) error {
values := url.Values{
"email": {emailAddress},
"channels": {channel},
@ -61,7 +66,7 @@ func (api *Client) InviteGuest(
"_attempts": {"1"},
}
_, err := adminRequest("invite", teamName, values, api.debug)
_, err := adminRequest(ctx, "invite", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to invite single-channel guest: %s", err)
}
@ -70,13 +75,12 @@ func (api *Client) InviteGuest(
}
// InviteRestricted invites a user to Slack as a restricted account
func (api *Client) InviteRestricted(
teamName string,
channel string,
firstName string,
lastName string,
emailAddress string,
) error {
func (api *Client) InviteRestricted(teamName, channel, firstName, lastName, emailAddress string) error {
return api.InviteRestrictedContext(context.Background(), teamName, channel, firstName, lastName, emailAddress)
}
// InviteRestrictedContext invites a user to Slack as a restricted account with a custom context
func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channel, firstName, lastName, emailAddress string) error {
values := url.Values{
"email": {emailAddress},
"channels": {channel},
@ -88,7 +92,7 @@ func (api *Client) InviteRestricted(
"_attempts": {"1"},
}
_, err := adminRequest("invite", teamName, values, api.debug)
_, err := adminRequest(ctx, "invite", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to restricted account: %s", err)
}
@ -97,12 +101,12 @@ func (api *Client) InviteRestricted(
}
// InviteToTeam invites a user to a Slack team
func (api *Client) InviteToTeam(
teamName string,
firstName string,
lastName string,
emailAddress string,
) error {
func (api *Client) InviteToTeam(teamName, firstName, lastName, emailAddress string) error {
return api.InviteToTeamContext(context.Background(), teamName, firstName, lastName, emailAddress)
}
// InviteToTeamContext invites a user to a Slack team with a custom context
func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName, lastName, emailAddress string) error {
values := url.Values{
"email": {emailAddress},
"first_name": {firstName},
@ -112,7 +116,7 @@ func (api *Client) InviteToTeam(
"_attempts": {"1"},
}
_, err := adminRequest("invite", teamName, values, api.debug)
_, err := adminRequest(ctx, "invite", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to invite to team: %s", err)
}
@ -121,7 +125,12 @@ func (api *Client) InviteToTeam(
}
// SetRegular enables the specified user
func (api *Client) SetRegular(teamName string, user string) error {
func (api *Client) SetRegular(teamName, user string) error {
return api.SetRegularContext(context.Background(), teamName, user)
}
// SetRegularContext enables the specified user with a custom context
func (api *Client) SetRegularContext(ctx context.Context, teamName, user string) error {
values := url.Values{
"user": {user},
"token": {api.config.token},
@ -129,7 +138,7 @@ func (api *Client) SetRegular(teamName string, user string) error {
"_attempts": {"1"},
}
_, err := adminRequest("setRegular", teamName, values, api.debug)
_, err := adminRequest(ctx, "setRegular", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err)
}
@ -138,7 +147,12 @@ func (api *Client) SetRegular(teamName string, user string) error {
}
// SendSSOBindingEmail sends an SSO binding email to the specified user
func (api *Client) SendSSOBindingEmail(teamName string, user string) error {
func (api *Client) SendSSOBindingEmail(teamName, user string) error {
return api.SendSSOBindingEmailContext(context.Background(), teamName, user)
}
// SendSSOBindingEmailContext sends an SSO binding email to the specified user with a custom context
func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, user string) error {
values := url.Values{
"user": {user},
"token": {api.config.token},
@ -146,7 +160,7 @@ func (api *Client) SendSSOBindingEmail(teamName string, user string) error {
"_attempts": {"1"},
}
_, err := adminRequest("sendSSOBind", teamName, values, api.debug)
_, err := adminRequest(ctx, "sendSSOBind", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err)
}
@ -156,6 +170,11 @@ func (api *Client) SendSSOBindingEmail(teamName string, user string) error {
// SetUltraRestricted converts a user into a single-channel guest
func (api *Client) SetUltraRestricted(teamName, uid, channel string) error {
return api.SetUltraRestrictedContext(context.Background(), teamName, uid, channel)
}
// SetUltraRestrictedContext converts a user into a single-channel guest with a custom context
func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid, channel string) error {
values := url.Values{
"user": {uid},
"channel": {channel},
@ -164,7 +183,7 @@ func (api *Client) SetUltraRestricted(teamName, uid, channel string) error {
"_attempts": {"1"},
}
_, err := adminRequest("setUltraRestricted", teamName, values, api.debug)
_, err := adminRequest(ctx, "setUltraRestricted", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to ultra-restrict account: %s", err)
}
@ -174,6 +193,11 @@ func (api *Client) SetUltraRestricted(teamName, uid, channel string) error {
// SetRestricted converts a user into a restricted account
func (api *Client) SetRestricted(teamName, uid string) error {
return api.SetRestrictedContext(context.Background(), teamName, uid)
}
// SetRestrictedContext converts a user into a restricted account with a custom context
func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid string) error {
values := url.Values{
"user": {uid},
"token": {api.config.token},
@ -181,7 +205,7 @@ func (api *Client) SetRestricted(teamName, uid string) error {
"_attempts": {"1"},
}
_, err := adminRequest("setRestricted", teamName, values, api.debug)
_, err := adminRequest(ctx, "setRestricted", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to restrict account: %s", err)
}

View File

@ -10,16 +10,34 @@ type AttachmentField struct {
Short bool `json:"short"`
}
// AttachmentAction is a button to be included in the attachment. Required when
// using message buttons and otherwise not useful. A maximum of 5 actions may be
// AttachmentAction is a button or menu to be included in the attachment. Required when
// using message buttons or menus and otherwise not useful. A maximum of 5 actions may be
// provided per attachment.
type AttachmentAction struct {
Name string `json:"name"` // Required.
Text string `json:"text"` // Required.
Style string `json:"style,omitempty"` // Optional. Allowed values: "default", "primary", "danger"
Type string `json:"type"` // Required. Must be set to "button"
Value string `json:"value,omitempty"` // Optional.
Confirm *ConfirmationField `json:"confirm,omitempty"` // Optional.
Name string `json:"name"` // Required.
Text string `json:"text"` // Required.
Style string `json:"style,omitempty"` // Optional. Allowed values: "default", "primary", "danger".
Type string `json:"type"` // Required. Must be set to "button" or "select".
Value string `json:"value,omitempty"` // Optional.
DataSource string `json:"data_source,omitempty"` // Optional.
MinQueryLength int `json:"min_query_length,omitempty"` // Optional. Default value is 1.
Options []AttachmentActionOption `json:"options,omitempty"` // Optional. Maximum of 100 options can be provided in each menu.
SelectedOptions []AttachmentActionOption `json:"selected_options,omitempty"` // Optional. The first element of this array will be set as the pre-selected option for this menu.
OptionGroups []AttachmentActionOptionGroup `json:"option_groups,omitempty"` // Optional.
Confirm *ConfirmationField `json:"confirm,omitempty"` // Optional.
}
// AttachmentActionOption the individual option to appear in action menu.
type AttachmentActionOption struct {
Text string `json:"text"` // Required.
Value string `json:"value"` // Required.
Description string `json:"description,omitempty"` // Optional. Up to 30 characters.
}
// AttachmentActionOptionGroup is a semi-hierarchal way to list available options to appear in action menu.
type AttachmentActionOptionGroup struct {
Text string `json:"text"` // Required.
Options []AttachmentActionOption `json:"options"` // Required.
}
// AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction)

View File

@ -1,6 +1,7 @@
package slack
import (
"context"
"errors"
"net/url"
)
@ -18,9 +19,9 @@ type botResponseFull struct {
SlackResponse
}
func botRequest(path string, values url.Values, debug bool) (*botResponseFull, error) {
func botRequest(ctx context.Context, path string, values url.Values, debug bool) (*botResponseFull, error) {
response := &botResponseFull{}
err := post(path, values, response, debug)
err := post(ctx, path, values, response, debug)
if err != nil {
return nil, err
}
@ -32,11 +33,16 @@ func botRequest(path string, values url.Values, debug bool) (*botResponseFull, e
// GetBotInfo will retrieve the complete bot information
func (api *Client) GetBotInfo(bot string) (*Bot, error) {
return api.GetBotInfoContext(context.Background(), bot)
}
// GetBotInfoContext will retrieve the complete bot information using a custom context
func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, error) {
values := url.Values{
"token": {api.config.token},
"bot": {bot},
}
response, err := botRequest("bots.info", values, api.debug)
response, err := botRequest(ctx, "bots.info", values, api.debug)
if err != nil {
return nil, err
}

View File

@ -1,6 +1,7 @@
package slack
import (
"context"
"errors"
"net/url"
"strconv"
@ -24,9 +25,9 @@ type Channel struct {
IsMember bool `json:"is_member"`
}
func channelRequest(path string, values url.Values, debug bool) (*channelResponseFull, error) {
func channelRequest(ctx context.Context, path string, values url.Values, debug bool) (*channelResponseFull, error) {
response := &channelResponseFull{}
err := post(path, values, response, debug)
err := post(ctx, path, values, response, debug)
if err != nil {
return nil, err
}
@ -38,11 +39,16 @@ func channelRequest(path string, values url.Values, debug bool) (*channelRespons
// ArchiveChannel archives the given channel
func (api *Client) ArchiveChannel(channel string) error {
return api.ArchiveChannelContext(context.Background(), channel)
}
// ArchiveChannelContext archives the given channel with a custom context
func (api *Client) ArchiveChannelContext(ctx context.Context, channel string) error {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
}
_, err := channelRequest("channels.archive", values, api.debug)
_, err := channelRequest(ctx, "channels.archive", values, api.debug)
if err != nil {
return err
}
@ -51,11 +57,16 @@ func (api *Client) ArchiveChannel(channel string) error {
// UnarchiveChannel unarchives the given channel
func (api *Client) UnarchiveChannel(channel string) error {
return api.UnarchiveChannelContext(context.Background(), channel)
}
// UnarchiveChannelContext unarchives the given channel with a custom context
func (api *Client) UnarchiveChannelContext(ctx context.Context, channel string) error {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
}
_, err := channelRequest("channels.unarchive", values, api.debug)
_, err := channelRequest(ctx, "channels.unarchive", values, api.debug)
if err != nil {
return err
}
@ -64,11 +75,16 @@ func (api *Client) UnarchiveChannel(channel string) error {
// CreateChannel creates a channel with the given name and returns a *Channel
func (api *Client) CreateChannel(channel string) (*Channel, error) {
return api.CreateChannelContext(context.Background(), channel)
}
// CreateChannelContext creates a channel with the given name and returns a *Channel with a custom context
func (api *Client) CreateChannelContext(ctx context.Context, channel string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"name": {channel},
}
response, err := channelRequest("channels.create", values, api.debug)
response, err := channelRequest(ctx, "channels.create", values, api.debug)
if err != nil {
return nil, err
}
@ -77,6 +93,11 @@ func (api *Client) CreateChannel(channel string) (*Channel, error) {
// GetChannelHistory retrieves the channel history
func (api *Client) GetChannelHistory(channel string, params HistoryParameters) (*History, error) {
return api.GetChannelHistoryContext(context.Background(), channel, params)
}
// GetChannelHistoryContext retrieves the channel history with a custom context
func (api *Client) GetChannelHistoryContext(ctx context.Context, channel string, params HistoryParameters) (*History, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
@ -104,7 +125,7 @@ func (api *Client) GetChannelHistory(channel string, params HistoryParameters) (
values.Add("unreads", "0")
}
}
response, err := channelRequest("channels.history", values, api.debug)
response, err := channelRequest(ctx, "channels.history", values, api.debug)
if err != nil {
return nil, err
}
@ -113,11 +134,16 @@ func (api *Client) GetChannelHistory(channel string, params HistoryParameters) (
// GetChannelInfo retrieves the given channel
func (api *Client) GetChannelInfo(channel string) (*Channel, error) {
return api.GetChannelInfoContext(context.Background(), channel)
}
// GetChannelInfoContext retrieves the given channel with a custom context
func (api *Client) GetChannelInfoContext(ctx context.Context, channel string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
}
response, err := channelRequest("channels.info", values, api.debug)
response, err := channelRequest(ctx, "channels.info", values, api.debug)
if err != nil {
return nil, err
}
@ -126,12 +152,17 @@ func (api *Client) GetChannelInfo(channel string) (*Channel, error) {
// InviteUserToChannel invites a user to a given channel and returns a *Channel
func (api *Client) InviteUserToChannel(channel, user string) (*Channel, error) {
return api.InviteUserToChannelContext(context.Background(), channel, user)
}
// InviteUserToChannelCustom invites a user to a given channel and returns a *Channel with a custom context
func (api *Client) InviteUserToChannelContext(ctx context.Context, channel, user string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"user": {user},
}
response, err := channelRequest("channels.invite", values, api.debug)
response, err := channelRequest(ctx, "channels.invite", values, api.debug)
if err != nil {
return nil, err
}
@ -140,11 +171,16 @@ func (api *Client) InviteUserToChannel(channel, user string) (*Channel, error) {
// JoinChannel joins the currently authenticated user to a channel
func (api *Client) JoinChannel(channel string) (*Channel, error) {
return api.JoinChannelContext(context.Background(), channel)
}
// JoinChannelContext joins the currently authenticated user to a channel with a custom context
func (api *Client) JoinChannelContext(ctx context.Context, channel string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"name": {channel},
}
response, err := channelRequest("channels.join", values, api.debug)
response, err := channelRequest(ctx, "channels.join", values, api.debug)
if err != nil {
return nil, err
}
@ -153,11 +189,16 @@ func (api *Client) JoinChannel(channel string) (*Channel, error) {
// LeaveChannel makes the authenticated user leave the given channel
func (api *Client) LeaveChannel(channel string) (bool, error) {
return api.LeaveChannelContext(context.Background(), channel)
}
// LeaveChannelContext makes the authenticated user leave the given channel with a custom context
func (api *Client) LeaveChannelContext(ctx context.Context, channel string) (bool, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
}
response, err := channelRequest("channels.leave", values, api.debug)
response, err := channelRequest(ctx, "channels.leave", values, api.debug)
if err != nil {
return false, err
}
@ -169,12 +210,17 @@ func (api *Client) LeaveChannel(channel string) (bool, error) {
// KickUserFromChannel kicks a user from a given channel
func (api *Client) KickUserFromChannel(channel, user string) error {
return api.KickUserFromChannelContext(context.Background(), channel, user)
}
// KickUserFromChannelContext kicks a user from a given channel with a custom context
func (api *Client) KickUserFromChannelContext(ctx context.Context, channel, user string) error {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"user": {user},
}
_, err := channelRequest("channels.kick", values, api.debug)
_, err := channelRequest(ctx, "channels.kick", values, api.debug)
if err != nil {
return err
}
@ -183,13 +229,18 @@ func (api *Client) KickUserFromChannel(channel, user string) error {
// GetChannels retrieves all the channels
func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) {
return api.GetChannelsContext(context.Background(), excludeArchived)
}
// GetChannelsContext retrieves all the channels with a custom context
func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool) ([]Channel, error) {
values := url.Values{
"token": {api.config.token},
}
if excludeArchived {
values.Add("exclude_archived", "1")
}
response, err := channelRequest("channels.list", values, api.debug)
response, err := channelRequest(ctx, "channels.list", values, api.debug)
if err != nil {
return nil, err
}
@ -202,12 +253,18 @@ func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) {
// (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A
// timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout.
func (api *Client) SetChannelReadMark(channel, ts string) error {
return api.SetChannelReadMarkContext(context.Background(), channel, ts)
}
// SetChannelReadMarkContext sets the read mark of a given channel to a specific point with a custom context
// For more details see SetChannelReadMark documentation
func (api *Client) SetChannelReadMarkContext(ctx context.Context, channel, ts string) error {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"ts": {ts},
}
_, err := channelRequest("channels.mark", values, api.debug)
_, err := channelRequest(ctx, "channels.mark", values, api.debug)
if err != nil {
return err
}
@ -216,6 +273,11 @@ func (api *Client) SetChannelReadMark(channel, ts string) error {
// RenameChannel renames a given channel
func (api *Client) RenameChannel(channel, name string) (*Channel, error) {
return api.RenameChannelContext(context.Background(), channel, name)
}
// RenameChannelContext renames a given channel with a custom context
func (api *Client) RenameChannelContext(ctx context.Context, channel, name string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
@ -223,23 +285,26 @@ func (api *Client) RenameChannel(channel, name string) (*Channel, error) {
}
// XXX: the created entry in this call returns a string instead of a number
// so I may have to do some workaround to solve it.
response, err := channelRequest("channels.rename", values, api.debug)
response, err := channelRequest(ctx, "channels.rename", values, api.debug)
if err != nil {
return nil, err
}
return &response.Channel, nil
}
// SetChannelPurpose sets the channel purpose and returns the purpose that was
// successfully set
// SetChannelPurpose sets the channel purpose and returns the purpose that was successfully set
func (api *Client) SetChannelPurpose(channel, purpose string) (string, error) {
return api.SetChannelPurposeContext(context.Background(), channel, purpose)
}
// SetChannelPurposeContext sets the channel purpose and returns the purpose that was successfully set with a custom context
func (api *Client) SetChannelPurposeContext(ctx context.Context, channel, purpose string) (string, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"purpose": {purpose},
}
response, err := channelRequest("channels.setPurpose", values, api.debug)
response, err := channelRequest(ctx, "channels.setPurpose", values, api.debug)
if err != nil {
return "", err
}
@ -248,14 +313,38 @@ func (api *Client) SetChannelPurpose(channel, purpose string) (string, error) {
// SetChannelTopic sets the channel topic and returns the topic that was successfully set
func (api *Client) SetChannelTopic(channel, topic string) (string, error) {
return api.SetChannelTopicContext(context.Background(), channel, topic)
}
// SetChannelTopicContext sets the channel topic and returns the topic that was successfully set with a custom context
func (api *Client) SetChannelTopicContext(ctx context.Context, channel, topic string) (string, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"topic": {topic},
}
response, err := channelRequest("channels.setTopic", values, api.debug)
response, err := channelRequest(ctx, "channels.setTopic", values, api.debug)
if err != nil {
return "", err
}
return response.Topic, nil
}
// GetChannelReplies gets an entire thread (a message plus all the messages in reply to it).
func (api *Client) GetChannelReplies(channel, thread_ts string) ([]Message, error) {
return api.GetChannelRepliesContext(context.Background(), channel, thread_ts)
}
// GetChannelRepliesContext gets an entire thread (a message plus all the messages in reply to it) with a custom context
func (api *Client) GetChannelRepliesContext(ctx context.Context, channel, thread_ts string) ([]Message, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"thread_ts": {thread_ts},
}
response, err := channelRequest(ctx, "channels.replies", values, api.debug)
if err != nil {
return nil, err
}
return response.History.Messages, nil
}

View File

@ -1,6 +1,7 @@
package slack
import (
"context"
"encoding/json"
"errors"
"net/url"
@ -62,9 +63,102 @@ func NewPostMessageParameters() PostMessageParameters {
}
}
func chatRequest(path string, values url.Values, debug bool) (*chatResponseFull, error) {
// DeleteMessage deletes a message in a channel
func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) {
respChannel, respTimestamp, _, err := api.SendMessageContext(context.Background(), channel, MsgOptionDelete(messageTimestamp))
return respChannel, respTimestamp, err
}
// DeleteMessageContext deletes a message in a channel with a custom context
func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTimestamp string) (string, string, error) {
respChannel, respTimestamp, _, err := api.SendMessageContext(ctx, channel, MsgOptionDelete(messageTimestamp))
return respChannel, respTimestamp, err
}
// PostMessage sends a message to a channel.
// Message is escaped by default according to https://api.slack.com/docs/formatting
// Use http://davestevens.github.io/slack-message-builder/ to help crafting your message.
func (api *Client) PostMessage(channel, text string, params PostMessageParameters) (string, string, error) {
respChannel, respTimestamp, _, err := api.SendMessageContext(
context.Background(),
channel,
MsgOptionText(text, params.EscapeText),
MsgOptionAttachments(params.Attachments...),
MsgOptionPostMessageParameters(params),
)
return respChannel, respTimestamp, err
}
// PostMessageContext sends a message to a channel with a custom context
// For more details, see PostMessage documentation
func (api *Client) PostMessageContext(ctx context.Context, channel, text string, params PostMessageParameters) (string, string, error) {
respChannel, respTimestamp, _, err := api.SendMessageContext(
ctx,
channel,
MsgOptionText(text, params.EscapeText),
MsgOptionAttachments(params.Attachments...),
MsgOptionPostMessageParameters(params),
)
return respChannel, respTimestamp, err
}
// UpdateMessage updates a message in a channel
func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) {
return api.UpdateMessageContext(context.Background(), channel, timestamp, text)
}
// UpdateMessage updates a message in a channel
func (api *Client) UpdateMessageContext(ctx context.Context, channel, timestamp, text string) (string, string, string, error) {
return api.SendMessageContext(ctx, channel, MsgOptionUpdate(timestamp), MsgOptionText(text, true))
}
// SendMessage more flexible method for configuring messages.
func (api *Client) SendMessage(channel string, options ...MsgOption) (string, string, string, error) {
return api.SendMessageContext(context.Background(), channel, options...)
}
// SendMessageContext more flexible method for configuring messages with a custom context.
func (api *Client) SendMessageContext(ctx context.Context, channel string, options ...MsgOption) (string, string, string, error) {
channel, values, err := ApplyMsgOptions(api.config.token, channel, options...)
if err != nil {
return "", "", "", err
}
response, err := chatRequest(ctx, channel, values, api.debug)
if err != nil {
return "", "", "", err
}
return response.Channel, response.Timestamp, response.Text, nil
}
// ApplyMsgOptions utility function for debugging/testing chat requests.
func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) {
config := sendConfig{
mode: chatPostMessage,
values: url.Values{
"token": {token},
"channel": {channel},
},
}
for _, opt := range options {
if err := opt(&config); err != nil {
return string(config.mode), config.values, err
}
}
return string(config.mode), config.values, nil
}
func escapeMessage(message string) string {
replacer := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
return replacer.Replace(message)
}
func chatRequest(ctx context.Context, path string, values url.Values, debug bool) (*chatResponseFull, error) {
response := &chatResponseFull{}
err := post(path, values, response, debug)
err := post(ctx, path, values, response, debug)
if err != nil {
return nil, err
}
@ -74,98 +168,153 @@ func chatRequest(path string, values url.Values, debug bool) (*chatResponseFull,
return response, nil
}
// DeleteMessage deletes a message in a channel
func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"ts": {messageTimestamp},
}
response, err := chatRequest("chat.delete", values, api.debug)
if err != nil {
return "", "", err
}
return response.Channel, response.Timestamp, nil
type sendMode string
const (
chatUpdate sendMode = "chat.update"
chatPostMessage sendMode = "chat.postMessage"
chatDelete sendMode = "chat.delete"
)
type sendConfig struct {
mode sendMode
values url.Values
}
func escapeMessage(message string) string {
replacer := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
return replacer.Replace(message)
// MsgOption option provided when sending a message.
type MsgOption func(*sendConfig) error
// MsgOptionPost posts a messages, this is the default.
func MsgOptionPost() MsgOption {
return func(config *sendConfig) error {
config.mode = chatPostMessage
config.values.Del("ts")
return nil
}
}
// PostMessage sends a message to a channel.
// Message is escaped by default according to https://api.slack.com/docs/formatting
// Use http://davestevens.github.io/slack-message-builder/ to help crafting your message.
func (api *Client) PostMessage(channel, text string, params PostMessageParameters) (string, string, error) {
if params.EscapeText {
text = escapeMessage(text)
// MsgOptionUpdate updates a message based on the timestamp.
func MsgOptionUpdate(timestamp string) MsgOption {
return func(config *sendConfig) error {
config.mode = chatUpdate
config.values.Add("ts", timestamp)
return nil
}
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"text": {text},
}
// MsgOptionDelete deletes a message based on the timestamp.
func MsgOptionDelete(timestamp string) MsgOption {
return func(config *sendConfig) error {
config.mode = chatDelete
config.values.Add("ts", timestamp)
return nil
}
if params.Username != DEFAULT_MESSAGE_USERNAME {
values.Set("username", string(params.Username))
}
if params.AsUser != DEFAULT_MESSAGE_ASUSER {
values.Set("as_user", "true")
}
if params.Parse != DEFAULT_MESSAGE_PARSE {
values.Set("parse", string(params.Parse))
}
if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES {
values.Set("link_names", "1")
}
if params.Attachments != nil {
attachments, err := json.Marshal(params.Attachments)
if err != nil {
return "", "", err
}
// MsgOptionAsUser whether or not to send the message as the user.
func MsgOptionAsUser(b bool) MsgOption {
return func(config *sendConfig) error {
if b != DEFAULT_MESSAGE_ASUSER {
config.values.Set("as_user", "true")
}
values.Set("attachments", string(attachments))
return nil
}
if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS {
values.Set("unfurl_links", "true")
}
// I want to send a message with explicit `as_user` `true` and `unfurl_links` `false` in request.
// Because setting `as_user` to `true` will change the default value for `unfurl_links` to `true` on Slack API side.
if params.AsUser != DEFAULT_MESSAGE_ASUSER && params.UnfurlLinks == DEFAULT_MESSAGE_UNFURL_LINKS {
values.Set("unfurl_links", "false")
}
if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA {
values.Set("unfurl_media", "false")
}
if params.IconURL != DEFAULT_MESSAGE_ICON_URL {
values.Set("icon_url", params.IconURL)
}
if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI {
values.Set("icon_emoji", params.IconEmoji)
}
if params.Markdown != DEFAULT_MESSAGE_MARKDOWN {
values.Set("mrkdwn", "false")
}
if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP {
values.Set("thread_ts", params.ThreadTimestamp)
}
response, err := chatRequest("chat.postMessage", values, api.debug)
if err != nil {
return "", "", err
}
return response.Channel, response.Timestamp, nil
}
// UpdateMessage updates a message in a channel
func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"text": {escapeMessage(text)},
"ts": {timestamp},
// MsgOptionText provide the text for the message, optionally escape the provided
// text.
func MsgOptionText(text string, escape bool) MsgOption {
return func(config *sendConfig) error {
if escape {
text = escapeMessage(text)
}
config.values.Add("text", text)
return nil
}
}
// MsgOptionAttachments provide attachments for the message.
func MsgOptionAttachments(attachments ...Attachment) MsgOption {
return func(config *sendConfig) error {
if attachments == nil {
return nil
}
attachments, err := json.Marshal(attachments)
if err == nil {
config.values.Set("attachments", string(attachments))
}
return err
}
}
// MsgOptionEnableLinkUnfurl enables link unfurling
func MsgOptionEnableLinkUnfurl() MsgOption {
return func(config *sendConfig) error {
config.values.Set("unfurl_links", "true")
return nil
}
}
// MsgOptionDisableMediaUnfurl disables media unfurling.
func MsgOptionDisableMediaUnfurl() MsgOption {
return func(config *sendConfig) error {
config.values.Set("unfurl_media", "false")
return nil
}
}
// MsgOptionDisableMarkdown disables markdown.
func MsgOptionDisableMarkdown() MsgOption {
return func(config *sendConfig) error {
config.values.Set("mrkdwn", "false")
return nil
}
}
// MsgOptionPostMessageParameters maintain backwards compatibility.
func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
return func(config *sendConfig) error {
if params.Username != DEFAULT_MESSAGE_USERNAME {
config.values.Set("username", string(params.Username))
}
// never generates an error.
MsgOptionAsUser(params.AsUser)(config)
if params.Parse != DEFAULT_MESSAGE_PARSE {
config.values.Set("parse", string(params.Parse))
}
if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES {
config.values.Set("link_names", "1")
}
if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS {
config.values.Set("unfurl_links", "true")
}
// I want to send a message with explicit `as_user` `true` and `unfurl_links` `false` in request.
// Because setting `as_user` to `true` will change the default value for `unfurl_links` to `true` on Slack API side.
if params.AsUser != DEFAULT_MESSAGE_ASUSER && params.UnfurlLinks == DEFAULT_MESSAGE_UNFURL_LINKS {
config.values.Set("unfurl_links", "false")
}
if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA {
config.values.Set("unfurl_media", "false")
}
if params.IconURL != DEFAULT_MESSAGE_ICON_URL {
config.values.Set("icon_url", params.IconURL)
}
if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI {
config.values.Set("icon_emoji", params.IconEmoji)
}
if params.Markdown != DEFAULT_MESSAGE_MARKDOWN {
config.values.Set("mrkdwn", "false")
}
if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP {
config.values.Set("thread_ts", params.ThreadTimestamp)
}
return nil
}
response, err := chatRequest("chat.update", values, api.debug)
if err != nil {
return "", "", "", err
}
return response.Channel, response.Timestamp, response.Text, nil
}

View File

@ -1,6 +1,7 @@
package slack
import (
"context"
"errors"
"net/url"
"strconv"
@ -35,9 +36,9 @@ type dndTeamInfoResponse struct {
SlackResponse
}
func dndRequest(path string, values url.Values, debug bool) (*dndResponseFull, error) {
func dndRequest(ctx context.Context, path string, values url.Values, debug bool) (*dndResponseFull, error) {
response := &dndResponseFull{}
err := post(path, values, response, debug)
err := post(ctx, path, values, response, debug)
if err != nil {
return nil, err
}
@ -49,12 +50,17 @@ func dndRequest(path string, values url.Values, debug bool) (*dndResponseFull, e
// EndDND ends the user's scheduled Do Not Disturb session
func (api *Client) EndDND() error {
return api.EndDNDContext(context.Background())
}
// EndDNDContext ends the user's scheduled Do Not Disturb session with a custom context
func (api *Client) EndDNDContext(ctx context.Context) error {
values := url.Values{
"token": {api.config.token},
}
response := &SlackResponse{}
if err := post("dnd.endDnd", values, response, api.debug); err != nil {
if err := post(ctx, "dnd.endDnd", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
@ -65,11 +71,16 @@ func (api *Client) EndDND() error {
// EndSnooze ends the current user's snooze mode
func (api *Client) EndSnooze() (*DNDStatus, error) {
return api.EndSnoozeContext(context.Background())
}
// EndSnoozeContext ends the current user's snooze mode with a custom context
func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) {
values := url.Values{
"token": {api.config.token},
}
response, err := dndRequest("dnd.endSnooze", values, api.debug)
response, err := dndRequest(ctx, "dnd.endSnooze", values, api.debug)
if err != nil {
return nil, err
}
@ -78,13 +89,18 @@ func (api *Client) EndSnooze() (*DNDStatus, error) {
// GetDNDInfo provides information about a user's current Do Not Disturb settings.
func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) {
return api.GetDNDInfoContext(context.Background(), user)
}
// GetDNDInfoContext provides information about a user's current Do Not Disturb settings with a custom context.
func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDStatus, error) {
values := url.Values{
"token": {api.config.token},
}
if user != nil {
values.Set("user", *user)
}
response, err := dndRequest("dnd.info", values, api.debug)
response, err := dndRequest(ctx, "dnd.info", values, api.debug)
if err != nil {
return nil, err
}
@ -93,12 +109,17 @@ func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) {
// GetDNDTeamInfo provides information about a user's current Do Not Disturb settings.
func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error) {
return api.GetDNDTeamInfoContext(context.Background(), users)
}
// GetDNDTeamInfoContext provides information about a user's current Do Not Disturb settings with a custom context.
func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (map[string]DNDStatus, error) {
values := url.Values{
"token": {api.config.token},
"users": {strings.Join(users, ",")},
}
response := &dndTeamInfoResponse{}
if err := post("dnd.teamInfo", values, response, api.debug); err != nil {
if err := post(ctx, "dnd.teamInfo", values, response, api.debug); err != nil {
return nil, err
}
if !response.Ok {
@ -111,11 +132,17 @@ func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error)
// settings. If a snooze session is not already active for the user, invoking
// this method will begin one for the specified duration.
func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) {
return api.SetSnoozeContext(context.Background(), minutes)
}
// SetSnooze adjusts the snooze duration for a user's Do Not Disturb settings with a custom context.
// For more information see the SetSnooze docs
func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) {
values := url.Values{
"token": {api.config.token},
"num_minutes": {strconv.Itoa(minutes)},
}
response, err := dndRequest("dnd.setSnooze", values, api.debug)
response, err := dndRequest(ctx, "dnd.setSnooze", values, api.debug)
if err != nil {
return nil, err
}

View File

@ -1,6 +1,7 @@
package slack
import (
"context"
"errors"
"net/url"
)
@ -12,11 +13,16 @@ type emojiResponseFull struct {
// GetEmoji retrieves all the emojis
func (api *Client) GetEmoji() (map[string]string, error) {
return api.GetEmojiContext(context.Background())
}
// GetEmojiContext retrieves all the emojis with a custom context
func (api *Client) GetEmojiContext(ctx context.Context) (map[string]string, error) {
values := url.Values{
"token": {api.config.token},
}
response := &emojiResponseFull{}
err := post("emoji.list", values, response, api.debug)
err := post(ctx, "emoji.list", values, response, api.debug)
if err != nil {
return nil, err
}

View File

@ -1,7 +1,9 @@
package slack
import (
"context"
"errors"
"io"
"net/url"
"strconv"
"strings"
@ -86,10 +88,14 @@ type File struct {
IsStarred bool `json:"is_starred"`
}
// FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request
// FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request.
//
// There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large,
// or provide a local file path in File to upload it from your filesystem.
type FileUploadParameters struct {
File string
Content string
Reader io.Reader
Filetype string
Filename string
Title string
@ -130,9 +136,9 @@ func NewGetFilesParameters() GetFilesParameters {
}
}
func fileRequest(path string, values url.Values, debug bool) (*fileResponseFull, error) {
func fileRequest(ctx context.Context, path string, values url.Values, debug bool) (*fileResponseFull, error) {
response := &fileResponseFull{}
err := post(path, values, response, debug)
err := post(ctx, path, values, response, debug)
if err != nil {
return nil, err
}
@ -144,13 +150,18 @@ func fileRequest(path string, values url.Values, debug bool) (*fileResponseFull,
// GetFileInfo retrieves a file and related comments
func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment, *Paging, error) {
return api.GetFileInfoContext(context.Background(), fileID, count, page)
}
// GetFileInfoContext retrieves a file and related comments with a custom context
func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, page int) (*File, []Comment, *Paging, error) {
values := url.Values{
"token": {api.config.token},
"file": {fileID},
"count": {strconv.Itoa(count)},
"page": {strconv.Itoa(page)},
}
response, err := fileRequest("files.info", values, api.debug)
response, err := fileRequest(ctx, "files.info", values, api.debug)
if err != nil {
return nil, nil, nil, err
}
@ -159,6 +170,11 @@ func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment
// GetFiles retrieves all files according to the parameters given
func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) {
return api.GetFilesContext(context.Background(), params)
}
// GetFilesContext retrieves all files according to the parameters given with a custom context
func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameters) ([]File, *Paging, error) {
values := url.Values{
"token": {api.config.token},
}
@ -168,12 +184,11 @@ func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error)
if params.Channel != DEFAULT_FILES_CHANNEL {
values.Add("channel", params.Channel)
}
// XXX: this is broken. fix it with a proper unix timestamp
if params.TimestampFrom != DEFAULT_FILES_TS_FROM {
values.Add("ts_from", params.TimestampFrom.String())
values.Add("ts_from", strconv.FormatInt(int64(params.TimestampFrom), 10))
}
if params.TimestampTo != DEFAULT_FILES_TS_TO {
values.Add("ts_to", params.TimestampTo.String())
values.Add("ts_to", strconv.FormatInt(int64(params.TimestampTo), 10))
}
if params.Types != DEFAULT_FILES_TYPES {
values.Add("types", params.Types)
@ -184,7 +199,7 @@ func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error)
if params.Page != DEFAULT_FILES_PAGE {
values.Add("page", strconv.Itoa(params.Page))
}
response, err := fileRequest("files.list", values, api.debug)
response, err := fileRequest(ctx, "files.list", values, api.debug)
if err != nil {
return nil, nil, err
}
@ -193,6 +208,11 @@ func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error)
// UploadFile uploads a file
func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) {
return api.UploadFileContext(context.Background(), params)
}
// UploadFileContext uploads a file and setting a custom context
func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParameters) (file *File, err error) {
// Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More
// investigation needed, but for now this will do.
_, err = api.AuthTest()
@ -220,9 +240,11 @@ func (api *Client) UploadFile(params FileUploadParameters) (file *File, err erro
}
if params.Content != "" {
values.Add("content", params.Content)
err = post("files.upload", values, response, api.debug)
err = post(ctx, "files.upload", values, response, api.debug)
} else if params.File != "" {
err = postWithMultipartResponse("files.upload", params.File, values, response, api.debug)
err = postLocalWithMultipartResponse(ctx, "files.upload", params.File, "file", values, response, api.debug)
} else if params.Reader != nil {
err = postWithMultipartResponse(ctx, "files.upload", params.Filename, "file", values, params.Reader, response, api.debug)
}
if err != nil {
return nil, err
@ -235,11 +257,16 @@ func (api *Client) UploadFile(params FileUploadParameters) (file *File, err erro
// DeleteFile deletes a file
func (api *Client) DeleteFile(fileID string) error {
return api.DeleteFileContext(context.Background(), fileID)
}
// DeleteFileContext deletes a file with a custom context
func (api *Client) DeleteFileContext(ctx context.Context, fileID string) error {
values := url.Values{
"token": {api.config.token},
"file": {fileID},
}
_, err := fileRequest("files.delete", values, api.debug)
_, err := fileRequest(ctx, "files.delete", values, api.debug)
if err != nil {
return err
}
@ -249,11 +276,16 @@ func (api *Client) DeleteFile(fileID string) error {
// RevokeFilePublicURL disables public/external sharing for a file
func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) {
return api.RevokeFilePublicURLContext(context.Background(), fileID)
}
// RevokeFilePublicURLContext disables public/external sharing for a file with a custom context
func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string) (*File, error) {
values := url.Values{
"token": {api.config.token},
"file": {fileID},
}
response, err := fileRequest("files.revokePublicURL", values, api.debug)
response, err := fileRequest(ctx, "files.revokePublicURL", values, api.debug)
if err != nil {
return nil, err
}
@ -262,11 +294,16 @@ func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) {
// ShareFilePublicURL enabled public/external sharing for a file
func (api *Client) ShareFilePublicURL(fileID string) (*File, []Comment, *Paging, error) {
return api.ShareFilePublicURLContext(context.Background(), fileID)
}
// ShareFilePublicURLContext enabled public/external sharing for a file with a custom context
func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) (*File, []Comment, *Paging, error) {
values := url.Values{
"token": {api.config.token},
"file": {fileID},
}
response, err := fileRequest("files.sharedPublicURL", values, api.debug)
response, err := fileRequest(ctx, "files.sharedPublicURL", values, api.debug)
if err != nil {
return nil, nil, nil, err
}

View File

@ -1,6 +1,7 @@
package slack
import (
"context"
"errors"
"net/url"
"strconv"
@ -27,9 +28,9 @@ type groupResponseFull struct {
SlackResponse
}
func groupRequest(path string, values url.Values, debug bool) (*groupResponseFull, error) {
func groupRequest(ctx context.Context, path string, values url.Values, debug bool) (*groupResponseFull, error) {
response := &groupResponseFull{}
err := post(path, values, response, debug)
err := post(ctx, path, values, response, debug)
if err != nil {
return nil, err
}
@ -41,11 +42,16 @@ func groupRequest(path string, values url.Values, debug bool) (*groupResponseFul
// ArchiveGroup archives a private group
func (api *Client) ArchiveGroup(group string) error {
return api.ArchiveGroupContext(context.Background(), group)
}
// ArchiveGroup archives a private group
func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
_, err := groupRequest("groups.archive", values, api.debug)
_, err := groupRequest(ctx, "groups.archive", values, api.debug)
if err != nil {
return err
}
@ -54,11 +60,16 @@ func (api *Client) ArchiveGroup(group string) error {
// UnarchiveGroup unarchives a private group
func (api *Client) UnarchiveGroup(group string) error {
return api.UnarchiveGroupContext(context.Background(), group)
}
// UnarchiveGroup unarchives a private group
func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) error {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
_, err := groupRequest("groups.unarchive", values, api.debug)
_, err := groupRequest(ctx, "groups.unarchive", values, api.debug)
if err != nil {
return err
}
@ -67,11 +78,16 @@ func (api *Client) UnarchiveGroup(group string) error {
// CreateGroup creates a private group
func (api *Client) CreateGroup(group string) (*Group, error) {
return api.CreateGroupContext(context.Background(), group)
}
// CreateGroup creates a private group
func (api *Client) CreateGroupContext(ctx context.Context, group string) (*Group, error) {
values := url.Values{
"token": {api.config.token},
"name": {group},
}
response, err := groupRequest("groups.create", values, api.debug)
response, err := groupRequest(ctx, "groups.create", values, api.debug)
if err != nil {
return nil, err
}
@ -85,11 +101,17 @@ func (api *Client) CreateGroup(group string) (*Group, error) {
// 3. Creates a new group with the name of the existing group.
// 4. Adds all members of the existing group to the new group.
func (api *Client) CreateChildGroup(group string) (*Group, error) {
return api.CreateChildGroupContext(context.Background(), group)
}
// CreateChildGroup creates a new private group archiving the old one with a custom context
// For more information see CreateChildGroup
func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (*Group, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
response, err := groupRequest("groups.createChild", values, api.debug)
response, err := groupRequest(ctx, "groups.createChild", values, api.debug)
if err != nil {
return nil, err
}
@ -98,11 +120,16 @@ func (api *Client) CreateChildGroup(group string) (*Group, error) {
// CloseGroup closes a private group
func (api *Client) CloseGroup(group string) (bool, bool, error) {
return api.CloseGroupContext(context.Background(), group)
}
// CloseGroupContext closes a private group with a custom context
func (api *Client) CloseGroupContext(ctx context.Context, group string) (bool, bool, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
response, err := imRequest("groups.close", values, api.debug)
response, err := imRequest(ctx, "groups.close", values, api.debug)
if err != nil {
return false, false, err
}
@ -111,6 +138,11 @@ func (api *Client) CloseGroup(group string) (bool, bool, error) {
// GetGroupHistory fetches all the history for a private group
func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*History, error) {
return api.GetGroupHistoryContext(context.Background(), group, params)
}
// GetGroupHistoryContext fetches all the history for a private group with a custom context
func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, params HistoryParameters) (*History, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
@ -138,7 +170,7 @@ func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*His
values.Add("unreads", "0")
}
}
response, err := groupRequest("groups.history", values, api.debug)
response, err := groupRequest(ctx, "groups.history", values, api.debug)
if err != nil {
return nil, err
}
@ -147,12 +179,17 @@ func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*His
// InviteUserToGroup invites a specific user to a private group
func (api *Client) InviteUserToGroup(group, user string) (*Group, bool, error) {
return api.InviteUserToGroupContext(context.Background(), group, user)
}
// InviteUserToGroupContext invites a specific user to a private group with a custom context
func (api *Client) InviteUserToGroupContext(ctx context.Context, group, user string) (*Group, bool, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
"user": {user},
}
response, err := groupRequest("groups.invite", values, api.debug)
response, err := groupRequest(ctx, "groups.invite", values, api.debug)
if err != nil {
return nil, false, err
}
@ -161,11 +198,16 @@ func (api *Client) InviteUserToGroup(group, user string) (*Group, bool, error) {
// LeaveGroup makes authenticated user leave the group
func (api *Client) LeaveGroup(group string) error {
return api.LeaveGroupContext(context.Background(), group)
}
// LeaveGroupContext makes authenticated user leave the group with a custom context
func (api *Client) LeaveGroupContext(ctx context.Context, group string) error {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
_, err := groupRequest("groups.leave", values, api.debug)
_, err := groupRequest(ctx, "groups.leave", values, api.debug)
if err != nil {
return err
}
@ -174,12 +216,17 @@ func (api *Client) LeaveGroup(group string) error {
// KickUserFromGroup kicks a user from a group
func (api *Client) KickUserFromGroup(group, user string) error {
return api.KickUserFromGroupContext(context.Background(), group, user)
}
// KickUserFromGroupContext kicks a user from a group with a custom context
func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user string) error {
values := url.Values{
"token": {api.config.token},
"channel": {group},
"user": {user},
}
_, err := groupRequest("groups.kick", values, api.debug)
_, err := groupRequest(ctx, "groups.kick", values, api.debug)
if err != nil {
return err
}
@ -188,13 +235,18 @@ func (api *Client) KickUserFromGroup(group, user string) error {
// GetGroups retrieves all groups
func (api *Client) GetGroups(excludeArchived bool) ([]Group, error) {
return api.GetGroupsContext(context.Background(), excludeArchived)
}
// GetGroupsContext retrieves all groups with a custom context
func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) ([]Group, error) {
values := url.Values{
"token": {api.config.token},
}
if excludeArchived {
values.Add("exclude_archived", "1")
}
response, err := groupRequest("groups.list", values, api.debug)
response, err := groupRequest(ctx, "groups.list", values, api.debug)
if err != nil {
return nil, err
}
@ -203,11 +255,16 @@ func (api *Client) GetGroups(excludeArchived bool) ([]Group, error) {
// GetGroupInfo retrieves the given group
func (api *Client) GetGroupInfo(group string) (*Group, error) {
return api.GetGroupInfoContext(context.Background(), group)
}
// GetGroupInfoContext retrieves the given group with a custom context
func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Group, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
response, err := groupRequest("groups.info", values, api.debug)
response, err := groupRequest(ctx, "groups.info", values, api.debug)
if err != nil {
return nil, err
}
@ -220,12 +277,18 @@ func (api *Client) GetGroupInfo(group string) (*Group, error) {
// calls (just one per channel). This is useful for when reading scroll-back history, or following a busy live
// channel. A timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout.
func (api *Client) SetGroupReadMark(group, ts string) error {
return api.SetGroupReadMarkContext(context.Background(), group, ts)
}
// SetGroupReadMarkContext sets the read mark on a private group with a custom context
// For more details see SetGroupReadMark
func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string) error {
values := url.Values{
"token": {api.config.token},
"channel": {group},
"ts": {ts},
}
_, err := groupRequest("groups.mark", values, api.debug)
_, err := groupRequest(ctx, "groups.mark", values, api.debug)
if err != nil {
return err
}
@ -234,11 +297,16 @@ func (api *Client) SetGroupReadMark(group, ts string) error {
// OpenGroup opens a private group
func (api *Client) OpenGroup(group string) (bool, bool, error) {
return api.OpenGroupContext(context.Background(), group)
}
// OpenGroupContext opens a private group with a custom context
func (api *Client) OpenGroupContext(ctx context.Context, group string) (bool, bool, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
response, err := groupRequest("groups.open", values, api.debug)
response, err := groupRequest(ctx, "groups.open", values, api.debug)
if err != nil {
return false, false, err
}
@ -249,6 +317,11 @@ func (api *Client) OpenGroup(group string) (bool, bool, error) {
// XXX: They return a channel, not a group. What is this crap? :(
// Inconsistent api it seems.
func (api *Client) RenameGroup(group, name string) (*Channel, error) {
return api.RenameGroupContext(context.Background(), group, name)
}
// RenameGroupContext renames a group with a custom context
func (api *Client) RenameGroupContext(ctx context.Context, group, name string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
@ -256,22 +329,26 @@ func (api *Client) RenameGroup(group, name string) (*Channel, error) {
}
// XXX: the created entry in this call returns a string instead of a number
// so I may have to do some workaround to solve it.
response, err := groupRequest("groups.rename", values, api.debug)
response, err := groupRequest(ctx, "groups.rename", values, api.debug)
if err != nil {
return nil, err
}
return &response.Channel, nil
}
// SetGroupPurpose sets the group purpose
func (api *Client) SetGroupPurpose(group, purpose string) (string, error) {
return api.SetGroupPurposeContext(context.Background(), group, purpose)
}
// SetGroupPurposeContext sets the group purpose with a custom context
func (api *Client) SetGroupPurposeContext(ctx context.Context, group, purpose string) (string, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
"purpose": {purpose},
}
response, err := groupRequest("groups.setPurpose", values, api.debug)
response, err := groupRequest(ctx, "groups.setPurpose", values, api.debug)
if err != nil {
return "", err
}
@ -280,12 +357,17 @@ func (api *Client) SetGroupPurpose(group, purpose string) (string, error) {
// SetGroupTopic sets the group topic
func (api *Client) SetGroupTopic(group, topic string) (string, error) {
return api.SetGroupTopicContext(context.Background(), group, topic)
}
// SetGroupTopicContext sets the group topic with a custom context
func (api *Client) SetGroupTopicContext(ctx context.Context, group, topic string) (string, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
"topic": {topic},
}
response, err := groupRequest("groups.setTopic", values, api.debug)
response, err := groupRequest(ctx, "groups.setTopic", values, api.debug)
if err != nil {
return "", err
}

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

@ -1,6 +1,7 @@
package slack
import (
"context"
"errors"
"net/url"
"strconv"
@ -28,9 +29,9 @@ type IM struct {
IsUserDeleted bool `json:"is_user_deleted"`
}
func imRequest(path string, values url.Values, debug bool) (*imResponseFull, error) {
func imRequest(ctx context.Context, path string, values url.Values, debug bool) (*imResponseFull, error) {
response := &imResponseFull{}
err := post(path, values, response, debug)
err := post(ctx, path, values, response, debug)
if err != nil {
return nil, err
}
@ -42,11 +43,16 @@ func imRequest(path string, values url.Values, debug bool) (*imResponseFull, err
// CloseIMChannel closes the direct message channel
func (api *Client) CloseIMChannel(channel string) (bool, bool, error) {
return api.CloseIMChannelContext(context.Background(), channel)
}
// CloseIMChannelContext closes the direct message channel with a custom context
func (api *Client) CloseIMChannelContext(ctx context.Context, channel string) (bool, bool, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
}
response, err := imRequest("im.close", values, api.debug)
response, err := imRequest(ctx, "im.close", values, api.debug)
if err != nil {
return false, false, err
}
@ -56,11 +62,17 @@ func (api *Client) CloseIMChannel(channel string) (bool, bool, error) {
// OpenIMChannel opens a direct message channel to the user provided as argument
// Returns some status and the channel ID
func (api *Client) OpenIMChannel(user string) (bool, bool, string, error) {
return api.OpenIMChannelContext(context.Background(), user)
}
// OpenIMChannelContext opens a direct message channel to the user provided as argument with a custom context
// Returns some status and the channel ID
func (api *Client) OpenIMChannelContext(ctx context.Context, user string) (bool, bool, string, error) {
values := url.Values{
"token": {api.config.token},
"user": {user},
}
response, err := imRequest("im.open", values, api.debug)
response, err := imRequest(ctx, "im.open", values, api.debug)
if err != nil {
return false, false, "", err
}
@ -69,12 +81,17 @@ func (api *Client) OpenIMChannel(user string) (bool, bool, string, error) {
// MarkIMChannel sets the read mark of a direct message channel to a specific point
func (api *Client) MarkIMChannel(channel, ts string) (err error) {
return api.MarkIMChannelContext(context.Background(), channel, ts)
}
// MarkIMChannelContext sets the read mark of a direct message channel to a specific point with a custom context
func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string) (err error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"ts": {ts},
}
_, err = imRequest("im.mark", values, api.debug)
_, err = imRequest(ctx, "im.mark", values, api.debug)
if err != nil {
return err
}
@ -83,6 +100,11 @@ func (api *Client) MarkIMChannel(channel, ts string) (err error) {
// GetIMHistory retrieves the direct message channel history
func (api *Client) GetIMHistory(channel string, params HistoryParameters) (*History, error) {
return api.GetIMHistoryContext(context.Background(), channel, params)
}
// GetIMHistoryContext retrieves the direct message channel history with a custom context
func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, params HistoryParameters) (*History, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
@ -110,7 +132,7 @@ func (api *Client) GetIMHistory(channel string, params HistoryParameters) (*Hist
values.Add("unreads", "0")
}
}
response, err := imRequest("im.history", values, api.debug)
response, err := imRequest(ctx, "im.history", values, api.debug)
if err != nil {
return nil, err
}
@ -119,10 +141,15 @@ func (api *Client) GetIMHistory(channel string, params HistoryParameters) (*Hist
// GetIMChannels returns the list of direct message channels
func (api *Client) GetIMChannels() ([]IM, error) {
return api.GetIMChannelsContext(context.Background())
}
// GetIMChannelsContext returns the list of direct message channels with a custom context
func (api *Client) GetIMChannelsContext(ctx context.Context) ([]IM, error) {
values := url.Values{
"token": {api.config.token},
}
response, err := imRequest("im.list", values, api.debug)
response, err := imRequest(ctx, "im.list", values, api.debug)
if err != nil {
return nil, err
}

View File

@ -2,10 +2,11 @@ package slack
// OutgoingMessage is used for the realtime API, and seems incomplete.
type OutgoingMessage struct {
ID int `json:"id"`
Channel string `json:"channel,omitempty"`
Text string `json:"text,omitempty"`
Type string `json:"type,omitempty"`
ID int `json:"id"`
Channel string `json:"channel,omitempty"`
Text string `json:"text,omitempty"`
Type string `json:"type,omitempty"`
ThreadTimestamp string `json:"thread_ts,omitempty"`
}
// Message is an auxiliary type to allow us to have a message containing sub messages
@ -17,15 +18,16 @@ type Message struct {
// Msg contains information about a slack message
type Msg struct {
// Basic Message
Type string `json:"type,omitempty"`
Channel string `json:"channel,omitempty"`
User string `json:"user,omitempty"`
Text string `json:"text,omitempty"`
Timestamp string `json:"ts,omitempty"`
IsStarred bool `json:"is_starred,omitempty"`
PinnedTo []string `json:"pinned_to, omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
Edited *Edited `json:"edited,omitempty"`
Type string `json:"type,omitempty"`
Channel string `json:"channel,omitempty"`
User string `json:"user,omitempty"`
Text string `json:"text,omitempty"`
Timestamp string `json:"ts,omitempty"`
ThreadTimestamp string `json:"thread_ts,omitempty"`
IsStarred bool `json:"is_starred,omitempty"`
PinnedTo []string `json:"pinned_to, omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
Edited *Edited `json:"edited,omitempty"`
// Message Subtypes
SubType string `json:"subtype,omitempty"`
@ -56,6 +58,11 @@ type Msg struct {
// channel_archive, group_archive
Members []string `json:"members,omitempty"`
// channels.replies, groups.replies, im.replies, mpim.replies
ReplyCount int `json:"reply_count,omitempty"`
Replies []Reply `json:"replies,omitempty"`
ParentUserId string `json:"parent_user_id,omitempty"`
// file_share, file_comment, file_mention
File *File `json:"file,omitempty"`
@ -88,6 +95,12 @@ type Edited struct {
Timestamp string `json:"ts,omitempty"`
}
// Reply contains information about a reply for a thread
type Reply struct {
User string `json:"user,omitempty"`
Timestamp string `json:"ts,omitempty"`
}
// Event contains the event type
type Event struct {
Type string `json:"type,omitempty"`

View File

@ -2,8 +2,8 @@ package slack
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
@ -13,9 +13,22 @@ import (
"net/url"
"os"
"path/filepath"
"strings"
"time"
)
// HTTPRequester defines the minimal interface needed for an http.Client to be implemented.
//
// Use it in conjunction with the SetHTTPClient function to allow for other capabilities
// like a tracing http.Client
type HTTPRequester interface {
Do(*http.Request) (*http.Response, error)
}
var customHTTPClient HTTPRequester
// HTTPClient sets a custom http.Client
// deprecated: in favor of SetHTTPClient()
var HTTPClient = &http.Client{}
type WebResponse struct {
@ -29,40 +42,24 @@ func (s WebError) Error() string {
return string(s)
}
func fileUploadReq(path, fpath string, values url.Values) (*http.Request, error) {
fullpath, err := filepath.Abs(fpath)
if err != nil {
return nil, err
}
file, err := os.Open(fullpath)
if err != nil {
return nil, err
}
defer file.Close()
func fileUploadReq(ctx context.Context, path, fieldname, filename string, values url.Values, r io.Reader) (*http.Request, error) {
body := &bytes.Buffer{}
wr := multipart.NewWriter(body)
ioWriter, err := wr.CreateFormFile("file", filepath.Base(fullpath))
ioWriter, err := wr.CreateFormFile(fieldname, filename)
if err != nil {
wr.Close()
return nil, err
}
bytes, err := io.Copy(ioWriter, file)
_, err = io.Copy(ioWriter, r)
if err != nil {
wr.Close()
return nil, err
}
// Close the multipart writer or the footer won't be written
wr.Close()
stat, err := file.Stat()
if err != nil {
return nil, err
}
if bytes != stat.Size() {
return nil, errors.New("could not read the whole file")
}
req, err := http.NewRequest("POST", path, body)
req = req.WithContext(ctx)
if err != nil {
return nil, err
}
@ -90,9 +87,26 @@ func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error
return nil
}
func postWithMultipartResponse(path string, filepath string, values url.Values, intf interface{}, debug bool) error {
req, err := fileUploadReq(SLACK_API+path, filepath, values)
resp, err := HTTPClient.Do(req)
func postLocalWithMultipartResponse(ctx context.Context, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error {
fullpath, err := filepath.Abs(fpath)
if err != nil {
return err
}
file, err := os.Open(fullpath)
if err != nil {
return err
}
defer file.Close()
return postWithMultipartResponse(ctx, path, filepath.Base(fpath), fieldname, values, file, intf, debug)
}
func postWithMultipartResponse(ctx context.Context, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, debug bool) error {
req, err := fileUploadReq(ctx, SLACK_API+path, fieldname, name, values, r)
if err != nil {
return err
}
req = req.WithContext(ctx)
resp, err := getHTTPClient().Do(req)
if err != nil {
return err
}
@ -107,23 +121,37 @@ func postWithMultipartResponse(path string, filepath string, values url.Values,
return parseResponseBody(resp.Body, &intf, debug)
}
func postForm(endpoint string, values url.Values, intf interface{}, debug bool) error {
resp, err := HTTPClient.PostForm(endpoint, values)
func postForm(ctx context.Context, endpoint string, values url.Values, intf interface{}, debug bool) error {
reqBody := strings.NewReader(values.Encode())
req, err := http.NewRequest("POST", endpoint, reqBody)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req = req.WithContext(ctx)
resp, err := getHTTPClient().Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != 200 {
logResponse(resp, debug)
return fmt.Errorf("Slack server error: %s.", resp.Status)
}
return parseResponseBody(resp.Body, &intf, debug)
}
func post(path string, values url.Values, intf interface{}, debug bool) error {
return postForm(SLACK_API+path, values, intf, debug)
func post(ctx context.Context, path string, values url.Values, intf interface{}, debug bool) error {
return postForm(ctx, SLACK_API+path, values, intf, debug)
}
func parseAdminResponse(method string, teamName string, values url.Values, intf interface{}, debug bool) error {
func parseAdminResponse(ctx context.Context, method string, teamName string, values url.Values, intf interface{}, debug bool) error {
endpoint := fmt.Sprintf(SLACK_WEB_API_FORMAT, teamName, method, time.Now().Unix())
return postForm(endpoint, values, intf, debug)
return postForm(ctx, endpoint, values, intf, debug)
}
func logResponse(resp *http.Response, debug bool) error {
@ -133,8 +161,23 @@ func logResponse(resp *http.Response, debug bool) error {
return err
}
logger.Print(text)
logger.Print(string(text))
}
return nil
}
func getHTTPClient() HTTPRequester {
if customHTTPClient != nil {
return customHTTPClient
}
return HTTPClient
}
// SetHTTPClient allows you to specify a custom http.Client
// Use this instead of the package level HTTPClient variable if you want to use a custom client like the
// Stackdriver Trace HTTPClient https://godoc.org/cloud.google.com/go/trace#HTTPClient
func SetHTTPClient(client HTTPRequester) {
customHTTPClient = client
}

View File

@ -1,6 +1,7 @@
package slack
import (
"context"
"errors"
"net/url"
)
@ -30,7 +31,12 @@ type OAuthResponse struct {
// GetOAuthToken retrieves an AccessToken
func GetOAuthToken(clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) {
response, err := GetOAuthResponse(clientID, clientSecret, code, redirectURI, debug)
return GetOAuthTokenContext(context.Background(), clientID, clientSecret, code, redirectURI, debug)
}
// GetOAuthTokenContext retrieves an AccessToken with a custom context
func GetOAuthTokenContext(ctx context.Context, clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) {
response, err := GetOAuthResponseContext(ctx, clientID, clientSecret, code, redirectURI, debug)
if err != nil {
return "", "", err
}
@ -38,6 +44,10 @@ func GetOAuthToken(clientID, clientSecret, code, redirectURI string, debug bool)
}
func GetOAuthResponse(clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) {
return GetOAuthResponseContext(context.Background(), clientID, clientSecret, code, redirectURI, debug)
}
func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) {
values := url.Values{
"client_id": {clientID},
"client_secret": {clientSecret},
@ -45,7 +55,7 @@ func GetOAuthResponse(clientID, clientSecret, code, redirectURI string, debug bo
"redirect_uri": {redirectURI},
}
response := &OAuthResponse{}
err = post("oauth.access", values, response, debug)
err = post(ctx, "oauth.access", values, response, debug)
if err != nil {
return nil, err
}

View File

@ -1,6 +1,7 @@
package slack
import (
"context"
"errors"
"net/url"
)
@ -13,6 +14,11 @@ type listPinsResponseFull struct {
// AddPin pins an item in a channel
func (api *Client) AddPin(channel string, item ItemRef) error {
return api.AddPinContext(context.Background(), channel, item)
}
// AddPinContext pins an item in a channel with a custom context
func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemRef) error {
values := url.Values{
"channel": {channel},
"token": {api.config.token},
@ -27,7 +33,7 @@ func (api *Client) AddPin(channel string, item ItemRef) error {
values.Set("file_comment", string(item.Comment))
}
response := &SlackResponse{}
if err := post("pins.add", values, response, api.debug); err != nil {
if err := post(ctx, "pins.add", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
@ -38,6 +44,11 @@ func (api *Client) AddPin(channel string, item ItemRef) error {
// RemovePin un-pins an item from a channel
func (api *Client) RemovePin(channel string, item ItemRef) error {
return api.RemovePinContext(context.Background(), channel, item)
}
// RemovePinContext un-pins an item from a channel with a custom context
func (api *Client) RemovePinContext(ctx context.Context, channel string, item ItemRef) error {
values := url.Values{
"channel": {channel},
"token": {api.config.token},
@ -52,7 +63,7 @@ func (api *Client) RemovePin(channel string, item ItemRef) error {
values.Set("file_comment", string(item.Comment))
}
response := &SlackResponse{}
if err := post("pins.remove", values, response, api.debug); err != nil {
if err := post(ctx, "pins.remove", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
@ -63,12 +74,17 @@ func (api *Client) RemovePin(channel string, item ItemRef) error {
// ListPins returns information about the items a user reacted to.
func (api *Client) ListPins(channel string) ([]Item, *Paging, error) {
return api.ListPinsContext(context.Background(), channel)
}
// ListPinsContext returns information about the items a user reacted to with a custom context.
func (api *Client) ListPinsContext(ctx context.Context, channel string) ([]Item, *Paging, error) {
values := url.Values{
"channel": {channel},
"token": {api.config.token},
}
response := &listPinsResponseFull{}
err := post("pins.list", values, response, api.debug)
err := post(ctx, "pins.list", values, response, api.debug)
if err != nil {
return nil, nil, err
}

View File

@ -1,6 +1,7 @@
package slack
import (
"context"
"errors"
"net/url"
"strconv"
@ -129,6 +130,11 @@ func (res listReactionsResponseFull) extractReactedItems() []ReactedItem {
// AddReaction adds a reaction emoji to a message, file or file comment.
func (api *Client) AddReaction(name string, item ItemRef) error {
return api.AddReactionContext(context.Background(), name, item)
}
// AddReactionContext adds a reaction emoji to a message, file or file comment with a custom context.
func (api *Client) AddReactionContext(ctx context.Context, name string, item ItemRef) error {
values := url.Values{
"token": {api.config.token},
}
@ -148,7 +154,7 @@ func (api *Client) AddReaction(name string, item ItemRef) error {
values.Set("file_comment", string(item.Comment))
}
response := &SlackResponse{}
if err := post("reactions.add", values, response, api.debug); err != nil {
if err := post(ctx, "reactions.add", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
@ -159,6 +165,11 @@ func (api *Client) AddReaction(name string, item ItemRef) error {
// RemoveReaction removes a reaction emoji from a message, file or file comment.
func (api *Client) RemoveReaction(name string, item ItemRef) error {
return api.RemoveReactionContext(context.Background(), name, item)
}
// RemoveReactionContext removes a reaction emoji from a message, file or file comment with a custom context.
func (api *Client) RemoveReactionContext(ctx context.Context, name string, item ItemRef) error {
values := url.Values{
"token": {api.config.token},
}
@ -178,7 +189,7 @@ func (api *Client) RemoveReaction(name string, item ItemRef) error {
values.Set("file_comment", string(item.Comment))
}
response := &SlackResponse{}
if err := post("reactions.remove", values, response, api.debug); err != nil {
if err := post(ctx, "reactions.remove", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
@ -189,6 +200,11 @@ func (api *Client) RemoveReaction(name string, item ItemRef) error {
// GetReactions returns details about the reactions on an item.
func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) {
return api.GetReactionsContext(context.Background(), item, params)
}
// GetReactionsContext returns details about the reactions on an item with a custom context
func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) {
values := url.Values{
"token": {api.config.token},
}
@ -208,7 +224,7 @@ func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]
values.Set("full", strconv.FormatBool(params.Full))
}
response := &getReactionsResponseFull{}
if err := post("reactions.get", values, response, api.debug); err != nil {
if err := post(ctx, "reactions.get", values, response, api.debug); err != nil {
return nil, err
}
if !response.Ok {
@ -219,6 +235,11 @@ func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]
// ListReactions returns information about the items a user reacted to.
func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, *Paging, error) {
return api.ListReactionsContext(context.Background(), params)
}
// ListReactionsContext returns information about the items a user reacted to with a custom context.
func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, *Paging, error) {
values := url.Values{
"token": {api.config.token},
}
@ -235,7 +256,7 @@ func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem,
values.Add("full", strconv.FormatBool(params.Full))
}
response := &listReactionsResponseFull{}
err := post("reactions.list", values, response, api.debug)
err := post(ctx, "reactions.list", values, response, api.debug)
if err != nil {
return nil, nil, err
}

View File

@ -1,18 +1,58 @@
package slack
import (
"context"
"encoding/json"
"fmt"
"net/url"
"time"
)
// StartRTM calls the "rtm.start" endpoint and returns the provided URL and the full Info
// block.
// StartRTM calls the "rtm.start" endpoint and returns the provided URL and the full Info block.
//
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()`
// on it.
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
func (api *Client) StartRTM() (info *Info, websocketURL string, err error) {
return api.StartRTMContext(context.Background())
}
// StartRTMContext calls the "rtm.start" endpoint and returns the provided URL and the full Info block with a custom context.
//
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) {
response := &infoResponseFull{}
err = post("rtm.start", url.Values{"token": {api.config.token}}, response, api.debug)
err = post(ctx, "rtm.start", url.Values{"token": {api.config.token}}, response, api.debug)
if err != nil {
return nil, "", fmt.Errorf("post: %s", err)
}
if !response.Ok {
return nil, "", response.Error
}
// websocket.Dial does not accept url without the port (yet)
// Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3
// but slack returns the address with no port, so we have to fix it
api.Debugln("Using URL:", response.Info.URL)
websocketURL, err = websocketizeURLPort(response.Info.URL)
if err != nil {
return nil, "", fmt.Errorf("parsing response URL: %s", err)
}
return &response.Info, websocketURL, nil
}
// ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block.
//
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) {
return api.ConnectRTMContext(context.Background())
}
// ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block with a custom context.
//
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) {
response := &infoResponseFull{}
err = post(ctx, "rtm.connect", url.Values{"token": {api.config.token}}, response, api.debug)
if err != nil {
return nil, "", fmt.Errorf("post: %s", err)
}
@ -33,7 +73,33 @@ func (api *Client) StartRTM() (info *Info, websocketURL string, err error) {
}
// NewRTM returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol./
// Slack's websocket-based Real-Time Messaging protocol.
func (api *Client) NewRTM() *RTM {
return newRTM(api)
return api.NewRTMWithOptions(nil)
}
// NewRTMWithOptions returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol.
// This also allows to configure various options available for RTM API.
func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM {
result := &RTM{
Client: *api,
IncomingEvents: make(chan RTMEvent, 50),
outgoingMessages: make(chan OutgoingMessage, 20),
pings: make(map[int]time.Time),
isConnected: false,
wasIntentional: true,
killChannel: make(chan bool),
forcePing: make(chan bool),
rawEvents: make(chan json.RawMessage),
idGen: NewSafeID(1),
}
if options != nil {
result.useRTMStart = options.UseRTMStart
} else {
result.useRTMStart = true
}
return result
}

View File

@ -1,6 +1,7 @@
package slack
import (
"context"
"errors"
"net/url"
"strconv"
@ -80,7 +81,7 @@ func NewSearchParameters() SearchParameters {
}
}
func (api *Client) _search(path, query string, params SearchParameters, files, messages bool) (response *searchResponseFull, error error) {
func (api *Client) _search(ctx context.Context, path, query string, params SearchParameters, files, messages bool) (response *searchResponseFull, error error) {
values := url.Values{
"token": {api.config.token},
"query": {query},
@ -101,7 +102,7 @@ func (api *Client) _search(path, query string, params SearchParameters, files, m
values.Add("page", strconv.Itoa(params.Page))
}
response = &searchResponseFull{}
err := post(path, values, response, api.debug)
err := post(ctx, path, values, response, api.debug)
if err != nil {
return nil, err
}
@ -113,7 +114,11 @@ func (api *Client) _search(path, query string, params SearchParameters, files, m
}
func (api *Client) Search(query string, params SearchParameters) (*SearchMessages, *SearchFiles, error) {
response, err := api._search("search.all", query, params, true, true)
return api.SearchContext(context.Background(), query, params)
}
func (api *Client) SearchContext(ctx context.Context, query string, params SearchParameters) (*SearchMessages, *SearchFiles, error) {
response, err := api._search(ctx, "search.all", query, params, true, true)
if err != nil {
return nil, nil, err
}
@ -121,7 +126,11 @@ func (api *Client) Search(query string, params SearchParameters) (*SearchMessage
}
func (api *Client) SearchFiles(query string, params SearchParameters) (*SearchFiles, error) {
response, err := api._search("search.files", query, params, true, false)
return api.SearchFilesContext(context.Background(), query, params)
}
func (api *Client) SearchFilesContext(ctx context.Context, query string, params SearchParameters) (*SearchFiles, error) {
response, err := api._search(ctx, "search.files", query, params, true, false)
if err != nil {
return nil, err
}
@ -129,7 +138,11 @@ func (api *Client) SearchFiles(query string, params SearchParameters) (*SearchFi
}
func (api *Client) SearchMessages(query string, params SearchParameters) (*SearchMessages, error) {
response, err := api._search("search.messages", query, params, false, true)
return api.SearchMessagesContext(context.Background(), query, params)
}
func (api *Client) SearchMessagesContext(ctx context.Context, query string, params SearchParameters) (*SearchMessages, error) {
response, err := api._search(ctx, "search.messages", query, params, false, true)
if err != nil {
return nil, err
}

View File

@ -1,6 +1,7 @@
package slack
import (
"context"
"errors"
"log"
"net/url"
@ -54,8 +55,13 @@ func New(token string) *Client {
// AuthTest tests if the user is able to do authenticated requests or not
func (api *Client) AuthTest() (response *AuthTestResponse, error error) {
return api.AuthTestContext(context.Background())
}
// AuthTestContext tests if the user is able to do authenticated requests or not with a custom context
func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestResponse, error error) {
responseFull := &authTestResponseFull{}
err := post("auth.test", url.Values{"token": {api.config.token}}, responseFull, api.debug)
err := post(ctx, "auth.test", url.Values{"token": {api.config.token}}, responseFull, api.debug)
if err != nil {
return nil, err
}
@ -71,7 +77,7 @@ func (api *Client) AuthTest() (response *AuthTestResponse, error error) {
func (api *Client) SetDebug(debug bool) {
api.debug = debug
if debug && logger == nil {
logger = log.New(os.Stdout, "nlopes/slack", log.LstdFlags | log.Lshortfile)
logger = log.New(os.Stdout, "nlopes/slack", log.LstdFlags|log.Lshortfile)
}
}

View File

@ -1,6 +1,7 @@
package slack
import (
"context"
"errors"
"net/url"
"strconv"
@ -37,6 +38,11 @@ func NewStarsParameters() StarsParameters {
// AddStar stars an item in a channel
func (api *Client) AddStar(channel string, item ItemRef) error {
return api.AddStarContext(context.Background(), channel, item)
}
// AddStarContext stars an item in a channel with a custom context
func (api *Client) AddStarContext(ctx context.Context, channel string, item ItemRef) error {
values := url.Values{
"channel": {channel},
"token": {api.config.token},
@ -51,7 +57,7 @@ func (api *Client) AddStar(channel string, item ItemRef) error {
values.Set("file_comment", string(item.Comment))
}
response := &SlackResponse{}
if err := post("stars.add", values, response, api.debug); err != nil {
if err := post(ctx, "stars.add", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
@ -62,6 +68,11 @@ func (api *Client) AddStar(channel string, item ItemRef) error {
// RemoveStar removes a starred item from a channel
func (api *Client) RemoveStar(channel string, item ItemRef) error {
return api.RemoveStarContext(context.Background(), channel, item)
}
// RemoveStarContext removes a starred item from a channel with a custom context
func (api *Client) RemoveStarContext(ctx context.Context, channel string, item ItemRef) error {
values := url.Values{
"channel": {channel},
"token": {api.config.token},
@ -76,7 +87,7 @@ func (api *Client) RemoveStar(channel string, item ItemRef) error {
values.Set("file_comment", string(item.Comment))
}
response := &SlackResponse{}
if err := post("stars.remove", values, response, api.debug); err != nil {
if err := post(ctx, "stars.remove", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
@ -87,6 +98,11 @@ func (api *Client) RemoveStar(channel string, item ItemRef) error {
// ListStars returns information about the stars a user added
func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) {
return api.ListStarsContext(context.Background(), params)
}
// ListStarsContext returns information about the stars a user added with a custom context
func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, *Paging, error) {
values := url.Values{
"token": {api.config.token},
}
@ -100,7 +116,7 @@ func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) {
values.Add("page", strconv.Itoa(params.Page))
}
response := &listResponseFull{}
err := post("stars.list", values, response, api.debug)
err := post(ctx, "stars.list", values, response, api.debug)
if err != nil {
return nil, nil, err
}
@ -110,7 +126,9 @@ func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) {
return response.Items, &response.Paging, nil
}
// GetStarred returns a list of StarredItem items. The user then has to iterate over them and figure out what they should
// GetStarred returns a list of StarredItem items.
//
// The user then has to iterate over them and figure out what they should
// be looking at according to what is in the Type.
// for _, item := range items {
// switch c.Type {
@ -123,7 +141,14 @@ func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) {
// This function still exists to maintain backwards compatibility.
// I exposed it as returning []StarredItem, so it shall stay as StarredItem
func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, *Paging, error) {
items, paging, err := api.ListStars(params)
return api.GetStarredContext(context.Background(), params)
}
// GetStarredContext returns a list of StarredItem items with a custom context
//
// For more details see GetStarred
func (api *Client) GetStarredContext(ctx context.Context, params StarsParameters) ([]StarredItem, *Paging, error) {
items, paging, err := api.ListStarsContext(ctx, params)
if err != nil {
return nil, nil, err
}

View File

@ -1,14 +1,15 @@
package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
const (
DEFAULT_LOGINS_COUNT = 100
DEFAULT_LOGINS_PAGE = 1
DEFAULT_LOGINS_COUNT = 100
DEFAULT_LOGINS_PAGE = 1
)
type TeamResponse struct {
@ -26,11 +27,10 @@ type TeamInfo struct {
type LoginResponse struct {
Logins []Login `json:"logins"`
Paging `json:"paging"`
Paging `json:"paging"`
SlackResponse
}
type Login struct {
UserID string `json:"user_id"`
Username string `json:"username"`
@ -47,7 +47,6 @@ type Login struct {
type BillableInfoResponse struct {
BillableInfo map[string]BillingActive `json:"billable_info"`
SlackResponse
}
type BillingActive struct {
@ -56,8 +55,8 @@ type BillingActive struct {
// AccessLogParameters contains all the parameters necessary (including the optional ones) for a GetAccessLogs() request
type AccessLogParameters struct {
Count int
Page int
Count int
Page int
}
// NewAccessLogParameters provides an instance of AccessLogParameters with all the sane default values set
@ -68,10 +67,9 @@ func NewAccessLogParameters() AccessLogParameters {
}
}
func teamRequest(path string, values url.Values, debug bool) (*TeamResponse, error) {
func teamRequest(ctx context.Context, path string, values url.Values, debug bool) (*TeamResponse, error) {
response := &TeamResponse{}
err := post(path, values, response, debug)
err := post(ctx, path, values, response, debug)
if err != nil {
return nil, err
}
@ -83,9 +81,9 @@ func teamRequest(path string, values url.Values, debug bool) (*TeamResponse, err
return response, nil
}
func billableInfoRequest(path string, values url.Values, debug bool) (map[string]BillingActive, error) {
func billableInfoRequest(ctx context.Context, path string, values url.Values, debug bool) (map[string]BillingActive, error) {
response := &BillableInfoResponse{}
err := post(path, values, response, debug)
err := post(ctx, path, values, response, debug)
if err != nil {
return nil, err
}
@ -97,9 +95,9 @@ func billableInfoRequest(path string, values url.Values, debug bool) (map[string
return response.BillableInfo, nil
}
func accessLogsRequest(path string, values url.Values, debug bool) (*LoginResponse, error) {
func accessLogsRequest(ctx context.Context, path string, values url.Values, debug bool) (*LoginResponse, error) {
response := &LoginResponse{}
err := post(path, values, response, debug)
err := post(ctx, path, values, response, debug)
if err != nil {
return nil, err
}
@ -109,14 +107,18 @@ func accessLogsRequest(path string, values url.Values, debug bool) (*LoginRespon
return response, nil
}
// GetTeamInfo gets the Team Information of the user
func (api *Client) GetTeamInfo() (*TeamInfo, error) {
return api.GetTeamInfoContext(context.Background())
}
// GetTeamInfoContext gets the Team Information of the user with a custom context
func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) {
values := url.Values{
"token": {api.config.token},
}
response, err := teamRequest("team.info", values, api.debug)
response, err := teamRequest(ctx, "team.info", values, api.debug)
if err != nil {
return nil, err
}
@ -125,6 +127,11 @@ func (api *Client) GetTeamInfo() (*TeamInfo, error) {
// GetAccessLogs retrieves a page of logins according to the parameters given
func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging, error) {
return api.GetAccessLogsContext(context.Background(), params)
}
// GetAccessLogsContext retrieves a page of logins according to the parameters given with a custom context
func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, *Paging, error) {
values := url.Values{
"token": {api.config.token},
}
@ -134,7 +141,7 @@ func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging,
if params.Page != DEFAULT_LOGINS_PAGE {
values.Add("page", strconv.Itoa(params.Page))
}
response, err := accessLogsRequest("team.accessLogs", values, api.debug)
response, err := accessLogsRequest(ctx, "team.accessLogs", values, api.debug)
if err != nil {
return nil, nil, err
}
@ -142,19 +149,28 @@ func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging,
}
func (api *Client) GetBillableInfo(user string) (map[string]BillingActive, error) {
return api.GetBillableInfoContext(context.Background(), user)
}
func (api *Client) GetBillableInfoContext(ctx context.Context, user string) (map[string]BillingActive, error) {
values := url.Values{
"token": {api.config.token},
"user": {user},
"user": {user},
}
return billableInfoRequest("team.billableInfo", values, api.debug)
return billableInfoRequest(ctx, "team.billableInfo", values, api.debug)
}
// GetBillableInfoForTeam returns the billing_active status of all users on the team.
func (api *Client) GetBillableInfoForTeam() (map[string]BillingActive, error) {
return api.GetBillableInfoForTeamContext(context.Background())
}
// GetBillableInfoForTeamContext returns the billing_active status of all users on the team with a custom context
func (api *Client) GetBillableInfoForTeamContext(ctx context.Context) (map[string]BillingActive, error) {
values := url.Values{
"token": {api.config.token},
}
return billableInfoRequest("team.billableInfo", values, api.debug)
return billableInfoRequest(ctx, "team.billableInfo", values, api.debug)
}

210
vendor/github.com/nlopes/slack/usergroups.go generated vendored Normal file
View File

@ -0,0 +1,210 @@
package slack
import (
"context"
"errors"
"net/url"
"strings"
)
// UserGroup contains all the information of a user group
type UserGroup struct {
ID string `json:"id"`
TeamID string `json:"team_id"`
IsUserGroup bool `json:"is_usergroup"`
Name string `json:"name"`
Description string `json:"description"`
Handle string `json:"handle"`
IsExternal bool `json:"is_external"`
DateCreate JSONTime `json:"date_create"`
DateUpdate JSONTime `json:"date_update"`
DateDelete JSONTime `json:"date_delete"`
AutoType string `json:"auto_type"`
CreatedBy string `json:"created_by"`
UpdatedBy string `json:"updated_by"`
DeletedBy string `json:"deleted_by"`
Prefs UserGroupPrefs `json:"prefs"`
UserCount int `json:"user_count"`
}
// UserGroupPrefs contains default channels and groups (private channels)
type UserGroupPrefs struct {
Channels []string `json:"channels"`
Groups []string `json:"groups"`
}
type userGroupResponseFull struct {
UserGroups []UserGroup `json:"usergroups"`
UserGroup UserGroup `json:"usergroup"`
Users []string `json:"users"`
SlackResponse
}
func userGroupRequest(ctx context.Context, path string, values url.Values, debug bool) (*userGroupResponseFull, error) {
response := &userGroupResponseFull{}
err := post(ctx, path, values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// CreateUserGroup creates a new user group
func (api *Client) CreateUserGroup(userGroup UserGroup) (UserGroup, error) {
return api.CreateUserGroupContext(context.Background(), userGroup)
}
// CreateUserGroupContext creates a new user group with a custom context
func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) {
values := url.Values{
"token": {api.config.token},
"name": {userGroup.Name},
}
if userGroup.Handle != "" {
values["handle"] = []string{userGroup.Handle}
}
if userGroup.Description != "" {
values["description"] = []string{userGroup.Description}
}
if len(userGroup.Prefs.Channels) > 0 {
values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")}
}
response, err := userGroupRequest(ctx, "usergroups.create", values, api.debug)
if err != nil {
return UserGroup{}, err
}
return response.UserGroup, nil
}
// DisableUserGroup disables an existing user group
func (api *Client) DisableUserGroup(userGroup string) (UserGroup, error) {
return api.DisableUserGroupContext(context.Background(), userGroup)
}
// DisableUserGroupContext disables an existing user group with a custom context
func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) {
values := url.Values{
"token": {api.config.token},
"usergroup": {userGroup},
}
response, err := userGroupRequest(ctx, "usergroups.disable", values, api.debug)
if err != nil {
return UserGroup{}, err
}
return response.UserGroup, nil
}
// EnableUserGroup enables an existing user group
func (api *Client) EnableUserGroup(userGroup string) (UserGroup, error) {
return api.EnableUserGroupContext(context.Background(), userGroup)
}
// EnableUserGroupContext enables an existing user group with a custom context
func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) {
values := url.Values{
"token": {api.config.token},
"usergroup": {userGroup},
}
response, err := userGroupRequest(ctx, "usergroups.enable", values, api.debug)
if err != nil {
return UserGroup{}, err
}
return response.UserGroup, nil
}
// GetUserGroups returns a list of user groups for the team
func (api *Client) GetUserGroups() ([]UserGroup, error) {
return api.GetUserGroupsContext(context.Background())
}
// GetUserGroupsContext returns a list of user groups for the team with a custom context
func (api *Client) GetUserGroupsContext(ctx context.Context) ([]UserGroup, error) {
values := url.Values{
"token": {api.config.token},
}
response, err := userGroupRequest(ctx, "usergroups.list", values, api.debug)
if err != nil {
return nil, err
}
return response.UserGroups, nil
}
// UpdateUserGroup will update an existing user group
func (api *Client) UpdateUserGroup(userGroup UserGroup) (UserGroup, error) {
return api.UpdateUserGroupContext(context.Background(), userGroup)
}
// UpdateUserGroupContext will update an existing user group with a custom context
func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) {
values := url.Values{
"token": {api.config.token},
"usergroup": {userGroup.ID},
}
if userGroup.Name != "" {
values["name"] = []string{userGroup.Name}
}
if userGroup.Handle != "" {
values["handle"] = []string{userGroup.Handle}
}
if userGroup.Description != "" {
values["description"] = []string{userGroup.Description}
}
response, err := userGroupRequest(ctx, "usergroups.update", values, api.debug)
if err != nil {
return UserGroup{}, err
}
return response.UserGroup, nil
}
// GetUserGroupMembers will retrieve the current list of users in a group
func (api *Client) GetUserGroupMembers(userGroup string) ([]string, error) {
return api.GetUserGroupMembersContext(context.Background(), userGroup)
}
// GetUserGroupMembersContext will retrieve the current list of users in a group with a custom context
func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup string) ([]string, error) {
values := url.Values{
"token": {api.config.token},
"usergroup": {userGroup},
}
response, err := userGroupRequest(ctx, "usergroups.users.list", values, api.debug)
if err != nil {
return []string{}, err
}
return response.Users, nil
}
// UpdateUserGroupMembers will update the members of an existing user group
func (api *Client) UpdateUserGroupMembers(userGroup string, members string) (UserGroup, error) {
return api.UpdateUserGroupMembersContext(context.Background(), userGroup, members)
}
// UpdateUserGroupMembersContext will update the members of an existing user group with a custom context
func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup string, members string) (UserGroup, error) {
values := url.Values{
"token": {api.config.token},
"usergroup": {userGroup},
"users": {members},
}
response, err := userGroupRequest(ctx, "usergroups.users.update", values, api.debug)
if err != nil {
return UserGroup{}, err
}
return response.UserGroup, nil
}

View File

@ -1,10 +1,18 @@
package slack
import (
"context"
"encoding/json"
"errors"
"net/url"
)
const (
DEFAULT_USER_PHOTO_CROP_X = -1
DEFAULT_USER_PHOTO_CROP_Y = -1
DEFAULT_USER_PHOTO_CROP_W = -1
)
// UserProfile contains all the information details of a given user
type UserProfile struct {
FirstName string `json:"first_name"`
@ -23,6 +31,8 @@ type UserProfile struct {
Title string `json:"title"`
BotID string `json:"bot_id,omitempty"`
ApiAppID string `json:"api_app_id,omitempty"`
StatusText string `json:"status_text,omitempty"`
StatusEmoji string `json:"status_emoji,omitempty"`
}
// User contains all the information of a user
@ -97,9 +107,23 @@ type userResponseFull struct {
SlackResponse
}
func userRequest(path string, values url.Values, debug bool) (*userResponseFull, error) {
type UserSetPhotoParams struct {
CropX int
CropY int
CropW int
}
func NewUserSetPhotoParams() UserSetPhotoParams {
return UserSetPhotoParams{
CropX: DEFAULT_USER_PHOTO_CROP_X,
CropY: DEFAULT_USER_PHOTO_CROP_Y,
CropW: DEFAULT_USER_PHOTO_CROP_W,
}
}
func userRequest(ctx context.Context, path string, values url.Values, debug bool) (*userResponseFull, error) {
response := &userResponseFull{}
err := post(path, values, response, debug)
err := post(ctx, path, values, response, debug)
if err != nil {
return nil, err
}
@ -111,11 +135,16 @@ func userRequest(path string, values url.Values, debug bool) (*userResponseFull,
// GetUserPresence will retrieve the current presence status of given user.
func (api *Client) GetUserPresence(user string) (*UserPresence, error) {
return api.GetUserPresenceContext(context.Background(), user)
}
// GetUserPresenceContext will retrieve the current presence status of given user with a custom context.
func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*UserPresence, error) {
values := url.Values{
"token": {api.config.token},
"user": {user},
}
response, err := userRequest("users.getPresence", values, api.debug)
response, err := userRequest(ctx, "users.getPresence", values, api.debug)
if err != nil {
return nil, err
}
@ -124,11 +153,16 @@ func (api *Client) GetUserPresence(user string) (*UserPresence, error) {
// GetUserInfo will retrieve the complete user information
func (api *Client) GetUserInfo(user string) (*User, error) {
return api.GetUserInfoContext(context.Background(), user)
}
// GetUserInfoContext will retrieve the complete user information with a custom context
func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, error) {
values := url.Values{
"token": {api.config.token},
"user": {user},
}
response, err := userRequest("users.info", values, api.debug)
response, err := userRequest(ctx, "users.info", values, api.debug)
if err != nil {
return nil, err
}
@ -137,11 +171,16 @@ func (api *Client) GetUserInfo(user string) (*User, error) {
// GetUsers returns the list of users (with their detailed information)
func (api *Client) GetUsers() ([]User, error) {
return api.GetUsersContext(context.Background())
}
// GetUsersContext returns the list of users (with their detailed information) with a custom context
func (api *Client) GetUsersContext(ctx context.Context) ([]User, error) {
values := url.Values{
"token": {api.config.token},
"presence": {"1"},
}
response, err := userRequest("users.list", values, api.debug)
response, err := userRequest(ctx, "users.list", values, api.debug)
if err != nil {
return nil, err
}
@ -150,10 +189,15 @@ func (api *Client) GetUsers() ([]User, error) {
// SetUserAsActive marks the currently authenticated user as active
func (api *Client) SetUserAsActive() error {
return api.SetUserAsActiveContext(context.Background())
}
// SetUserAsActiveContext marks the currently authenticated user as active with a custom context
func (api *Client) SetUserAsActiveContext(ctx context.Context) error {
values := url.Values{
"token": {api.config.token},
}
_, err := userRequest("users.setActive", values, api.debug)
_, err := userRequest(ctx, "users.setActive", values, api.debug)
if err != nil {
return err
}
@ -162,11 +206,16 @@ func (api *Client) SetUserAsActive() error {
// SetUserPresence changes the currently authenticated user presence
func (api *Client) SetUserPresence(presence string) error {
return api.SetUserPresenceContext(context.Background(), presence)
}
// SetUserPresenceContext changes the currently authenticated user presence with a custom context
func (api *Client) SetUserPresenceContext(ctx context.Context, presence string) error {
values := url.Values{
"token": {api.config.token},
"presence": {presence},
}
_, err := userRequest("users.setPresence", values, api.debug)
_, err := userRequest(ctx, "users.setPresence", values, api.debug)
if err != nil {
return err
}
@ -176,11 +225,16 @@ func (api *Client) SetUserPresence(presence string) error {
// GetUserIdentity will retrieve user info available per identity scopes
func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) {
return api.GetUserIdentityContext(context.Background())
}
// GetUserIdentityContext will retrieve user info available per identity scopes with a custom context
func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityResponse, error) {
values := url.Values{
"token": {api.config.token},
}
response := &UserIdentityResponse{}
err := post("users.identity", values, response, api.debug)
err := post(ctx, "users.identity", values, response, api.debug)
if err != nil {
return nil, err
}
@ -189,3 +243,120 @@ func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) {
}
return response, nil
}
// SetUserPhoto changes the currently authenticated user's profile image
func (api *Client) SetUserPhoto(ctx context.Context, image string, params UserSetPhotoParams) error {
return api.SetUserPhoto(context.Background(), image, params)
}
// SetUserPhotoContext changes the currently authenticated user's profile image using a custom context
func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) error {
response := &SlackResponse{}
values := url.Values{
"token": {api.config.token},
}
if params.CropX != DEFAULT_USER_PHOTO_CROP_X {
values.Add("crop_x", string(params.CropX))
}
if params.CropY != DEFAULT_USER_PHOTO_CROP_Y {
values.Add("crop_y", string(params.CropY))
}
if params.CropW != DEFAULT_USER_PHOTO_CROP_W {
values.Add("crop_w", string(params.CropW))
}
err := postLocalWithMultipartResponse(ctx, "users.setPhoto", image, "image", values, response, api.debug)
if err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
}
// DeleteUserPhoto deletes the current authenticated user's profile image
func (api *Client) DeleteUserPhoto() error {
return api.DeleteUserPhotoContext(context.Background())
}
// DeleteUserPhotoContext deletes the current authenticated user's profile image with a custom context
func (api *Client) DeleteUserPhotoContext(ctx context.Context) error {
response := &SlackResponse{}
values := url.Values{
"token": {api.config.token},
}
err := post(ctx, "users.deletePhoto", values, response, api.debug)
if err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
}
// SetUserCustomStatus will set a custom status and emoji for the currently
// authenticated user. If statusEmoji is "" and statusText is not, the Slack API
// will automatically set it to ":speech_balloon:". Otherwise, if both are ""
// the Slack API will unset the custom status/emoji.
func (api *Client) SetUserCustomStatus(statusText, statusEmoji string) error {
return api.SetUserCustomStatusContext(context.Background(), statusText, statusEmoji)
}
// SetUserCustomStatusContext will set a custom status and emoji for the currently authenticated user with a custom context
//
// For more information see SetUserCustomStatus
func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, statusEmoji string) error {
// XXX(theckman): this anonymous struct is for making requests to the Slack
// API for setting and unsetting a User's Custom Status/Emoji. To change
// these values we must provide a JSON document as the profile POST field.
//
// We use an anonymous struct over UserProfile because to unset the values
// on the User's profile we cannot use the `json:"omitempty"` tag. This is
// because an empty string ("") is what's used to unset the values. Check
// out the API docs for more details:
//
// - https://api.slack.com/docs/presence-and-status#custom_status
profile, err := json.Marshal(
&struct {
StatusText string `json:"status_text"`
StatusEmoji string `json:"status_emoji"`
}{
StatusText: statusText,
StatusEmoji: statusEmoji,
},
)
if err != nil {
return err
}
values := url.Values{
"token": {api.config.token},
"profile": {string(profile)},
}
response := &userResponseFull{}
if err = post(ctx, "users.profile.set", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
}
// UnsetUserCustomStatus removes the custom status message for the currently
// authenticated user. This is a convenience method that wraps (*Client).SetUserCustomStatus().
func (api *Client) UnsetUserCustomStatus() error {
return api.UnsetUserCustomStatusContext(context.Background())
}
// UnsetUserCustomStatusContext removes the custom status message for the currently authenticated user
// with a custom context. This is a convenience method that wraps (*Client).SetUserCustomStatus().
func (api *Client) UnsetUserCustomStatusContext(ctx context.Context) error {
return api.SetUserCustomStatusContext(ctx, "", "")
}

View File

@ -17,7 +17,7 @@ const (
// RTM represents a managed websocket connection. It also supports
// all the methods of the `Client` type.
//
// Create this element with Client's NewRTM().
// Create this element with Client's NewRTM() or NewRTMWithOptions(*RTMOptions)
type RTM struct {
idGen IDGenerator
pings map[int]time.Time
@ -38,23 +38,23 @@ type RTM struct {
// UserDetails upon connection
info *Info
// useRTMStart should be set to true if you want to use
// rtm.start to connect to Slack, otherwise it will use
// rtm.connect
useRTMStart bool
}
// NewRTM returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol.
func newRTM(api *Client) *RTM {
return &RTM{
Client: *api,
IncomingEvents: make(chan RTMEvent, 50),
outgoingMessages: make(chan OutgoingMessage, 20),
pings: make(map[int]time.Time),
isConnected: false,
wasIntentional: true,
killChannel: make(chan bool),
forcePing: make(chan bool),
rawEvents: make(chan json.RawMessage),
idGen: NewSafeID(1),
}
// RTMOptions allows configuration of various options available for RTM messaging
//
// This structure will evolve in time so please make sure you are always using the
// named keys for every entry available as per Go 1 compatibility promise adding fields
// to this structure should not be considered a breaking change.
type RTMOptions struct {
// UseRTMStart set to true in order to use rtm.start or false to use rtm.connect
// As of 11th July 2017 you should prefer setting this to false, see:
// https://api.slack.com/changelog/2017-04-start-using-rtm-connect-and-stop-using-rtm-start
UseRTMStart bool
}
// Disconnect and wait, blocking until a successful disconnection.

View File

@ -29,7 +29,7 @@ func (rtm *RTM) ManageConnection() {
connectionCount++
// start trying to connect
// the returned err is already passed onto the IncomingEvents channel
info, conn, err := rtm.connect(connectionCount)
info, conn, err := rtm.connect(connectionCount, rtm.useRTMStart)
// if err != nil then the connection is sucessful - otherwise it is
// fatal
if err != nil {
@ -64,7 +64,9 @@ func (rtm *RTM) ManageConnection() {
// connect attempts to connect to the slack websocket API. It handles any
// errors that occur while connecting and will return once a connection
// has been successfully opened.
func (rtm *RTM) connect(connectionCount int) (*Info, *websocket.Conn, error) {
// If useRTMStart is false then it uses rtm.connect to create the connection,
// otherwise it uses rtm.start.
func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocket.Conn, error) {
// used to provide exponential backoff wait time with jitter before trying
// to connect to slack again
boff := &backoff{
@ -81,7 +83,7 @@ func (rtm *RTM) connect(connectionCount int) (*Info, *websocket.Conn, error) {
ConnectionCount: connectionCount,
}}
// attempt to start the connection
info, conn, err := rtm.startRTMAndDial()
info, conn, err := rtm.startRTMAndDial(useRTMStart)
if err == nil {
return info, conn, nil
}
@ -105,10 +107,19 @@ func (rtm *RTM) connect(connectionCount int) (*Info, *websocket.Conn, error) {
}
}
// startRTMAndDial attemps to connect to the slack websocket. It returns the
// full information returned by the "rtm.start" method on the slack API.
func (rtm *RTM) startRTMAndDial() (*Info, *websocket.Conn, error) {
info, url, err := rtm.StartRTM()
// startRTMAndDial attempts to connect to the slack websocket. If useRTMStart is true,
// then it returns the full information returned by the "rtm.start" method on the
// slack API. Else it uses the "rtm.connect" method to connect
func (rtm *RTM) startRTMAndDial(useRTMStart bool) (*Info, *websocket.Conn, error) {
var info *Info
var url string
var err error
if useRTMStart {
info, url, err = rtm.StartRTM()
} else {
info, url, err = rtm.ConnectRTM()
}
if err != nil {
return nil, nil, err
}
@ -291,6 +302,8 @@ func (rtm *RTM) handleRawEvent(rawEvent json.RawMessage) {
rtm.IncomingEvents <- RTMEvent{"hello", &HelloEvent{}}
case "pong":
rtm.handlePong(rawEvent)
case "desktop_notification":
rtm.Debugln("Received desktop notification, ignoring")
default:
rtm.handleEvent(event.Type, rawEvent)
}

View File

@ -76,8 +76,12 @@ type UserChangeEvent struct {
// EmojiChangedEvent represents the emoji changed event
type EmojiChangedEvent struct {
Type string `json:"type"`
EventTimestamp string `json:"event_ts"`
Type string `json:"type"`
SubType string `json:"subtype"`
Name string `json:"name"`
Names []string `json:"names"`
Value string `json:"value"`
EventTimestamp string `json:"event_ts"`
}
// CommandsChangedEvent represents the commands changed event

View File

@ -14,7 +14,7 @@ There are also some interesting projects using termbox-go:
- [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.
- [httopd](https://github.com/verdverm/httopd) is top for httpd logs.
- [mop](https://github.com/michaeldv/mop) is stock market tracker for hackers.
- [mop](https://github.com/mop-tracker/mop) is stock market tracker for hackers.
- [termui](https://github.com/gizak/termui) is a terminal dashboard.
- [termloop](https://github.com/JoelOtter/termloop) is a terminal game engine.
- [xterm-color-chart](https://github.com/kutuluk/xterm-color-chart) is a XTerm 256 color chart.
@ -27,6 +27,12 @@ There are also some interesting projects using termbox-go:
- [lf](https://github.com/gokcehan/lf) is a terminal file manager
- [rat](https://github.com/ericfreese/rat) lets you compose shell commands to build terminal applications.
- [httplab](https://github.com/gchaincl/httplab) An interactive web server.
- [tetris](https://github.com/MichaelS11/tetris) Go Tetris with AI option
- [wot](https://github.com/kyu-suke/wot) Wait time during command is completed.
- [2048-go](https://github.com/1984weed/2048-go) is 2048 in Go
- [jv](https://github.com/maxzender/jv) helps you view JSON on the command-line.
- [pinger](https://github.com/hirose31/pinger) helps you to monitor numerous hosts using ICMP ECHO_REQUEST.
- [vixl44](https://github.com/sebashwa/vixl44) lets you create pixel art inside your terminal using vim movements
### API reference
[godoc.org/github.com/nsf/termbox-go](http://godoc.org/github.com/nsf/termbox-go)

View File

@ -418,12 +418,12 @@ func SetInputMode(mode InputMode) InputMode {
//
// 3. Output216 => [1..216]
// This mode supports the 3rd range of the 256 mode only.
// But you dont need to provide an offset.
// But you don't need to provide an offset.
//
// 4. OutputGrayscale => [1..26]
// This mode supports the 4th range of the 256 mode
// and black and white colors from 3th range of the 256 mode
// But you dont need to provide an offset.
// But you don't need to provide an offset.
//
// In all modes, 0x00 represents the default color.
//

View File

@ -63,6 +63,8 @@ const (
mouse_lmb = 0x1
mouse_rmb = 0x2
mouse_mmb = 0x4 | 0x8 | 0x10
SM_CXMIN = 28
SM_CYMIN = 29
)
func (this coord) uintptr() uintptr {
@ -70,6 +72,7 @@ func (this coord) uintptr() uintptr {
}
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var moduser32 = syscall.NewLazyDLL("user32.dll")
var is_cjk = runewidth.IsEastAsian()
var (
@ -91,6 +94,7 @@ var (
proc_create_event = kernel32.NewProc("CreateEventW")
proc_wait_for_multiple_objects = kernel32.NewProc("WaitForMultipleObjects")
proc_set_event = kernel32.NewProc("SetEvent")
get_system_metrics = moduser32.NewProc("GetSystemMetrics")
)
func set_console_active_screen_buffer(h syscall.Handle) (err error) {
@ -397,15 +401,44 @@ func get_term_size(out syscall.Handle) coord {
return tmp_info.size
}
func get_win_min_size(out syscall.Handle) coord {
x, _, err := get_system_metrics.Call(SM_CXMIN)
y, _, err := get_system_metrics.Call(SM_CYMIN)
if x == 0 || y == 0 {
if err != nil {
panic(err)
}
}
return coord{
x: short(x),
y: short(y),
}
}
func get_win_size(out syscall.Handle) coord {
err := get_console_screen_buffer_info(out, &tmp_info)
if err != nil {
panic(err)
}
return coord{
min_size := get_win_min_size(out)
size := coord{
x: tmp_info.window.right - tmp_info.window.left + 1,
y: tmp_info.window.bottom - tmp_info.window.top + 1,
}
if size.x < min_size.x {
size.x = min_size.x
}
if size.y < min_size.y {
size.y = min_size.y
}
return size
}
func update_size_maybe() {

21
vendor/github.com/renstrom/fuzzysearch/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Peter Renström
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

167
vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go generated vendored Normal file
View File

@ -0,0 +1,167 @@
// Fuzzy searching allows for flexibly matching a string with partial input,
// useful for filtering data very quickly based on lightweight user input.
package fuzzy
import (
"unicode"
"unicode/utf8"
)
var noop = func(r rune) rune { return r }
// Match returns true if source matches target using a fuzzy-searching
// algorithm. Note that it doesn't implement Levenshtein distance (see
// RankMatch instead), but rather a simplified version where there's no
// approximation. The method will return true only if each character in the
// source can be found in the target and occurs after the preceding matches.
func Match(source, target string) bool {
return match(source, target, noop)
}
// MatchFold is a case-insensitive version of Match.
func MatchFold(source, target string) bool {
return match(source, target, unicode.ToLower)
}
func match(source, target string, fn func(rune) rune) bool {
lenDiff := len(target) - len(source)
if lenDiff < 0 {
return false
}
if lenDiff == 0 && source == target {
return true
}
Outer:
for _, r1 := range source {
for i, r2 := range target {
if fn(r1) == fn(r2) {
target = target[i+utf8.RuneLen(r2):]
continue Outer
}
}
return false
}
return true
}
// Find will return a list of strings in targets that fuzzy matches source.
func Find(source string, targets []string) []string {
return find(source, targets, noop)
}
// FindFold is a case-insensitive version of Find.
func FindFold(source string, targets []string) []string {
return find(source, targets, unicode.ToLower)
}
func find(source string, targets []string, fn func(rune) rune) []string {
var matches []string
for _, target := range targets {
if match(source, target, fn) {
matches = append(matches, target)
}
}
return matches
}
// RankMatch is similar to Match except it will measure the Levenshtein
// distance between the source and the target and return its result. If there
// was no match, it will return -1.
// Given the requirements of match, RankMatch only needs to perform a subset of
// the Levenshtein calculation, only deletions need be considered, required
// additions and substitutions would fail the match test.
func RankMatch(source, target string) int {
return rank(source, target, noop)
}
// RankMatchFold is a case-insensitive version of RankMatch.
func RankMatchFold(source, target string) int {
return rank(source, target, unicode.ToLower)
}
func rank(source, target string, fn func(rune) rune) int {
lenDiff := len(target) - len(source)
if lenDiff < 0 {
return -1
}
if lenDiff == 0 && source == target {
return 0
}
runeDiff := 0
Outer:
for _, r1 := range source {
for i, r2 := range target {
if fn(r1) == fn(r2) {
target = target[i+utf8.RuneLen(r2):]
continue Outer
} else {
runeDiff++
}
}
return -1
}
// Count up remaining char
for len(target) > 0 {
target = target[utf8.RuneLen(rune(target[0])):]
runeDiff++
}
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})
}
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})
}
return r
}
type Rank struct {
// Source is used as the source for matching.
Source string
// Target is the word matched against.
Target string
// Distance is the Levenshtein distance between Source and Target.
Distance int
}
type Ranks []Rank
func (r Ranks) Len() int {
return len(r)
}
func (r Ranks) Swap(i, j int) {
r[i], r[j] = r[j], r[i]
}
func (r Ranks) Less(i, j int) bool {
return r[i].Distance < r[j].Distance
}

View File

@ -0,0 +1,43 @@
package fuzzy
// LevenshteinDistance measures the difference between two strings.
// The Levenshtein distance between two words is the minimum number of
// single-character edits (i.e. insertions, deletions or substitutions)
// required to change one word into the other.
//
// This implemention is optimized to use O(min(m,n)) space and is based on the
// optimized C version found here:
// http://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Levenshtein_distance#C
func LevenshteinDistance(s, t string) int {
r1, r2 := []rune(s), []rune(t)
column := make([]int, len(r1)+1)
for y := 1; y <= len(r1); y++ {
column[y] = y
}
for x := 1; x <= len(r2); x++ {
column[0] = x
for y, lastDiag := 1, x-1; y <= len(r1); y++ {
oldDiag := column[y]
cost := 0
if r1[y-1] != r2[x-1] {
cost = 1
}
column[y] = min(column[y]+1, column[y-1]+1, lastDiag+cost)
lastDiag = oldDiag
}
}
return column[len(r1)]
}
func min(a, b, c int) int {
if a < b && a < c {
return a
} else if b < c {
return b
}
return c
}

210
vendor/vendor.json vendored
View File

@ -2,6 +2,58 @@
"comment": "",
"ignore": "test",
"package": [
{
"path": "bufio",
"revision": ""
},
{
"path": "bytes",
"revision": ""
},
{
"path": "context",
"revision": ""
},
{
"path": "crypto/rand",
"revision": ""
},
{
"path": "crypto/sha1",
"revision": ""
},
{
"path": "crypto/tls",
"revision": ""
},
{
"path": "encoding/base64",
"revision": ""
},
{
"path": "encoding/binary",
"revision": ""
},
{
"path": "encoding/hex",
"revision": ""
},
{
"path": "encoding/json",
"revision": ""
},
{
"path": "errors",
"revision": ""
},
{
"path": "flag",
"revision": ""
},
{
"path": "fmt",
"revision": ""
},
{
"checksumSHA1": "CbpC2ha+GTTuROMyyLVd/L3O+8Y=",
"path": "github.com/erroneousboat/termui",
@ -9,10 +61,10 @@
"revisionTime": "2017-09-23T11:51:41Z"
},
{
"checksumSHA1": "7hln62oZPZmyqEmgXaybf9WxQ7A=",
"checksumSHA1": "zpFCi2nWiwR5F2INAJOvQqsj7lY=",
"path": "github.com/maruel/panicparse/stack",
"revision": "868abbf1ebac0fb2760cd9a410a5bd2f5afb2f76",
"revisionTime": "2017-07-16T23:31:26Z"
"revision": "766956aceb8ff49664065ae50bef0ae8a0a83ec4",
"revisionTime": "2017-11-29T15:16:18Z"
},
{
"checksumSHA1": "cJE7dphDlam/i7PhnsyosNWtbd4=",
@ -33,16 +85,158 @@
"revisionTime": "2017-07-25T12:17:30Z"
},
{
"checksumSHA1": "2si62NpJ4Rw8vVb7+LOfnKJ3y2Q=",
"checksumSHA1": "Zi8hWUMkKtii1fc6YaGgoYAssIw=",
"path": "github.com/nsf/termbox-go",
"revision": "4ed959e0540971545eddb8c75514973d670cf739",
"revisionTime": "2017-07-10T10:34:07Z"
"revision": "aa4a75b1c20a2b03751b1a9f7e41d58bd6f71c43",
"revisionTime": "2017-11-04T16:23:16Z"
},
{
"checksumSHA1": "DF3jZEw4lCq/SEaC7DIl/R+7S70=",
"path": "github.com/renstrom/fuzzysearch/fuzzy",
"revision": "2d205ac6ec17a839a94bdbfd16d2fa6c6dada2e0",
"revisionTime": "2016-03-31T20:48:55Z"
},
{
"path": "go/ast",
"revision": ""
},
{
"path": "go/parser",
"revision": ""
},
{
"path": "go/token",
"revision": ""
},
{
"checksumSHA1": "7EZyXN0EmZLgGxZxK01IJua4c8o=",
"path": "golang.org/x/net/websocket",
"revision": "f5079bd7f6f74e23c4d65efa0f4ce14cbd6a3c0f",
"revisionTime": "2017-07-19T21:11:51Z"
"revision": "a8b9294777976932365dabb6640cf1468d95c70f",
"revisionTime": "2017-11-29T19:21:16Z"
},
{
"path": "html",
"revision": ""
},
{
"path": "image",
"revision": ""
},
{
"path": "io",
"revision": ""
},
{
"path": "io/ioutil",
"revision": ""
},
{
"path": "log",
"revision": ""
},
{
"path": "math",
"revision": ""
},
{
"path": "math/rand",
"revision": ""
},
{
"path": "mime/multipart",
"revision": ""
},
{
"path": "net",
"revision": ""
},
{
"path": "net/http",
"revision": ""
},
{
"path": "net/http/httputil",
"revision": ""
},
{
"path": "net/url",
"revision": ""
},
{
"path": "os",
"revision": ""
},
{
"path": "os/signal",
"revision": ""
},
{
"path": "os/user",
"revision": ""
},
{
"path": "path",
"revision": ""
},
{
"path": "path/filepath",
"revision": ""
},
{
"path": "reflect",
"revision": ""
},
{
"path": "regexp",
"revision": ""
},
{
"path": "runtime",
"revision": ""
},
{
"path": "runtime/debug",
"revision": ""
},
{
"path": "sort",
"revision": ""
},
{
"path": "strconv",
"revision": ""
},
{
"path": "strings",
"revision": ""
},
{
"path": "sync",
"revision": ""
},
{
"path": "syscall",
"revision": ""
},
{
"path": "time",
"revision": ""
},
{
"path": "unicode",
"revision": ""
},
{
"path": "unicode/utf16",
"revision": ""
},
{
"path": "unicode/utf8",
"revision": ""
},
{
"path": "unsafe",
"revision": ""
}
],
"rootPath": "github.com/erroneousboat/slack-term"

View File

@ -1,48 +0,0 @@
package views
import (
"github.com/erroneousboat/termui"
"github.com/erroneousboat/slack-term/components"
"github.com/erroneousboat/slack-term/service"
)
type View struct {
Input *components.Input
Chat *components.Chat
Channels *components.Channels
Mode *components.Mode
}
func CreateChatView(svc *service.SlackService) *View {
input := components.CreateInput()
channels := components.CreateChannels(svc, input.Par.Height)
chat := components.CreateChat(
svc,
input.Par.Height,
svc.SlackChannels[channels.SelectedChannel],
svc.Channels[channels.SelectedChannel],
)
mode := components.CreateMode()
view := &View{
Input: input,
Channels: channels,
Chat: chat,
Mode: mode,
}
return view
}
func (v *View) Refresh() {
termui.Render(
v.Input,
v.Chat,
v.Channels,
v.Mode,
)
}

73
views/view.go Normal file
View File

@ -0,0 +1,73 @@
package views
import (
"github.com/erroneousboat/termui"
"github.com/erroneousboat/slack-term/components"
"github.com/erroneousboat/slack-term/config"
"github.com/erroneousboat/slack-term/service"
)
type View struct {
Config *config.Config
Input *components.Input
Chat *components.Chat
Channels *components.Channels
Mode *components.Mode
Debug *components.Debug
}
func CreateView(config *config.Config, svc *service.SlackService) *View {
// Create Input component
input := components.CreateInputComponent()
// Channels: create the component
channels := components.CreateChannelsComponent(input.Par.Height)
// Channels: fill the component
slackChans := svc.GetChannels()
channels.SetChannels(slackChans)
// Chat: create the component
chat := components.CreateChatComponent(input.Par.Height)
// Chat: fill the component
msgs := svc.GetMessages(
svc.GetSlackChannel(channels.SelectedChannel),
chat.GetMaxItems(),
)
var strMsgs []string
for _, msg := range msgs {
strMsgs = append(strMsgs, msg.ToString())
}
chat.SetMessages(strMsgs)
chat.SetBorderLabel(svc.Channels[channels.SelectedChannel].GetChannelName())
// Debug: create the component
debug := components.CreateDebugComponent(input.Par.Height)
// Mode: create the component
mode := components.CreateModeComponent()
view := &View{
Config: config,
Input: input,
Channels: channels,
Chat: chat,
Mode: mode,
Debug: debug,
}
return view
}
func (v *View) Refresh() {
termui.Render(
v.Input,
v.Chat,
v.Channels,
v.Mode,
)
}