Compare commits

..

1 Commits

Author SHA1 Message Date
erroneousboat
825e88b407 Implement rate limited user presence resolver
Reference #167
2018-12-25 14:00:39 +01:00
190 changed files with 4115 additions and 35324 deletions

View File

@ -1,6 +0,0 @@
Please read [CONTRIBUTING.md](https://github.com/erroneousboat/slack-term/blob/master/CONTRIBUTING.md)
---
Version:
Installation method:

View File

@ -1 +0,0 @@
Please read [CONTRIBUTING.md](https://github.com/erroneousboat/slack-term/blob/master/CONTRIBUTING.md)

View File

@ -1,31 +0,0 @@
FROM golang:alpine as builder
ENV PATH /go/bin:/usr/local/go/bin:$PATH
ENV GOPATH /go
RUN apk add --no-cache \
ca-certificates
COPY . /go/src/github.com/erroneousboat/slack-term
RUN set -x \
&& apk add --no-cache --virtual .build-deps \
git \
gcc \
libc-dev \
libgcc \
make \
&& cd /go/src/github.com/erroneousboat/slack-term \
&& make build \
&& mv ./bin/slack-term /usr/bin/slack-term \
&& apk del .build-deps \
&& rm -rf /go
FROM alpine:latest
ENV USER root
COPY --from=builder /usr/bin/slack-term /usr/bin/slack-term
COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs
ENTRYPOINT stty cols 25 && slack-term -config config

95
Gopkg.lock generated Normal file
View File

@ -0,0 +1,95 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
digest = "1:592569a314f98130ac3085243fdbe46f278d3e54c95ce9e0bde9c6b908db82c4"
name = "github.com/0xAX/notificator"
packages = ["."]
pruneopts = "UT"
revision = "88d57ee9043ba88d6a62e437fa15dda1ca0d2b59"
[[projects]]
digest = "1:c2ee2bebf300b3c6d998802bdefe0422a65bcdcdd5c902e1ed518448c56e8f98"
name = "github.com/erroneousboat/termui"
packages = ["."]
pruneopts = "UT"
revision = "80f245cdfa0488883a3e8602bf3f0c8a3c889a22"
[[projects]]
digest = "1:cee8e8ac80df6373e7daa11baf1f98c1b6f7242c49ccae7e1ec34a971dc408d9"
name = "github.com/gorilla/websocket"
packages = ["."]
pruneopts = "UT"
revision = "3ff3320c2a1756a3691521efc290b4701575147c"
version = "v1.3.0"
[[projects]]
digest = "1:f614e627d47e1276989de725dc5e433504a8b5498850711c9d3fcec3bfa7c943"
name = "github.com/maruel/panicparse"
packages = ["stack"]
pruneopts = "UT"
revision = "785840568bdc7faa0dfb1cd6c643207f03271f64"
version = "v1.1.1"
[[projects]]
digest = "1:cdb899c199f907ac9fb50495ec71212c95cb5b0e0a8ee0800da0238036091033"
name = "github.com/mattn/go-runewidth"
packages = ["."]
pruneopts = "UT"
revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb"
version = "v0.0.3"
[[projects]]
branch = "master"
digest = "1:e68cd472b96cdf7c9f6971ac41bcc1d4d3b23d67c2a31d2399446e295bc88ae9"
name = "github.com/mitchellh/go-wordwrap"
packages = ["."]
pruneopts = "UT"
revision = "ad45545899c7b13c020ea92b2072220eefad42b8"
[[projects]]
branch = "master"
digest = "1:410e126b7e96640ac0c41bb49bad7dbf2d1c081aa06fd2c75cdb9e65765fae9b"
name = "github.com/nlopes/slack"
packages = ["."]
pruneopts = "UT"
revision = "7cfa5619e6becd3db5dfb8e26c06798918e123b2"
[[projects]]
branch = "master"
digest = "1:f335d800550786b6f51ddaedb9d1107a7a72f4a2195e5b039dd7c0e103e119bc"
name = "github.com/nsf/termbox-go"
packages = ["."]
pruneopts = "UT"
revision = "b66b20ab708e289ff1eb3e218478302e6aec28ce"
[[projects]]
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = "UT"
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
digest = "1:3fd3d634f6815f19ac4b2c5e16d28ec9aa4584d0bba25d1ee6c424d813cca22a"
name = "github.com/renstrom/fuzzysearch"
packages = ["fuzzy"]
pruneopts = "UT"
revision = "b18e754edff4833912ef4dce9eaca885bd3f0de1"
version = "v1.0.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/0xAX/notificator",
"github.com/erroneousboat/termui",
"github.com/mattn/go-runewidth",
"github.com/nlopes/slack",
"github.com/nsf/termbox-go",
"github.com/renstrom/fuzzysearch/fuzzy",
]
solver-name = "gps-cdcl"
solver-version = 1

50
Gopkg.toml Normal file
View File

@ -0,0 +1,50 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/erroneousboat/termui"
revision = "80f245cdfa0488883a3e8602bf3f0c8a3c889a22"
[[constraint]]
name = "github.com/mattn/go-runewidth"
version = "0.0.2"
[[constraint]]
name = "github.com/nlopes/slack"
branch = "master"
[[constraint]]
name = "github.com/nsf/termbox-go"
branch = "master"
[[constraint]]
name = "github.com/renstrom/fuzzysearch"
version = "1.0.0"
[prune]
go-tests = true
unused-packages = true

View File

@ -22,10 +22,6 @@ dev: build
# We're setting the OS to linux (in case someone builds the binary on Mac or
# Windows)
#
# `-mod=vendor`
# This ensures that the build process will use the modules in the vendor
# folder.
#
# `-a`
# Force rebuilding of package, all import will be rebuilt with cgo disabled,
# which means all the imports will be rebuilt with cgo disabled.
@ -43,17 +39,17 @@ dev: build
# Location of the source files
build:
@ echo "+ $@"
@ CGO_ENABLED=0 go build -mod=vendor -a -installsuffix cgo -o ./bin/slack-term .
@ CGO_ENABLED=0 go build -a -installsuffix cgo -o ./bin/slack-term .
# Cross-compile
# http://dave.cheney.net/2015/08/22/cross-compilation-with-go-1-5
build-linux:
@ echo "+ $@"
@ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -mod=vendor -a -installsuffix cgo -o ./bin/slack-term-linux-amd64 .
@ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -a -installsuffix cgo -o ./bin/slack-term-linux-amd64 .
build-mac:
@ echo "+ $@"
@ GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -mod=vendor -a -installsuffix cgo -o ./bin/slack-term-darwin-amd64 .
@ GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -a -installsuffix cgo -o ./bin/slack-term-darwin-amd64 .
run: build
@ echo "+ $@"
@ -63,11 +59,6 @@ install:
@ echo "+ $@"
@ go install .
modules:
@ echo "+ $@"
@ go mod tidy
@ go mod vendor
build-all: build build-linux build-mac
.PHONY: default test build build-linux build-mac run install

View File

@ -29,29 +29,15 @@ $ cd $GOPATH/src/github.com/erroneousboat/slack-term
$ go install .
```
#### Via docker
You can also run it with docker, make sure you have a valid config file
on your host system.
```bash
docker run -it -v [config-file]:/config erroneousboat/slack-term
```
Setup
-----
1. Get a slack token, click [here](https://github.com/erroneousboat/slack-term/wiki#running-slack-term-without-legacy-tokens)
1. Get a slack token, click [here](https://api.slack.com/docs/oauth-test-tokens)
2. Running `slack-term` for the first time, will create a default config file at
`~/.config/slack-term/config`.
```bash
$ slack-term
```
3. Update the config file and update your `slack_token` For more configuration
options of the `config` file, see the [wiki](https://github.com/erroneousboat/slack-term/wiki).
2. Create a `.slack-term` file, and place it in your home directory. Below is
an example of such a file. You are only required to specify a
`slack_token`. For more configuration options of the `.slack-term` file,
see the [wiki](https://github.com/erroneousboat/slack-term/wiki).
```javascript
{
@ -63,7 +49,7 @@ Usage
-----
When everything is setup correctly you can run `slack-term` with the following
command:
command:
```bash
$ slack-term
@ -73,7 +59,7 @@ Default Key Mapping
-------------------
Below are the default key-mappings for `slack-term`, you can change them
in your `config` file.
in your `.slack-term` file.
| mode | key | action |
|---------|-----------|----------------------------|
@ -83,9 +69,6 @@ in your `config` file.
| command | `j` | move channel cursor down |
| command | `g` | move channel cursor top |
| command | `G` | move channel cursor bottom |
| command | `K` | thread up |
| command | `J` | thread down |
| command | `G` | move channel cursor bottom |
| command | `pg-up` | scroll chat pane up |
| command | `ctrl-b` | scroll chat pane up |
| command | `ctrl-u` | scroll chat pane up |
@ -94,7 +77,6 @@ in your `config` file.
| command | `ctrl-d` | scroll chat pane down |
| command | `n` | next search match |
| command | `N` | previous search match |
| command | `,` | jump to next notification |
| command | `q` | quit |
| command | `f1` | help |
| insert | `left` | move input cursor left |

View File

@ -5,7 +5,7 @@ import (
"html"
"github.com/erroneousboat/termui"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/renstrom/fuzzysearch/fuzzy"
)
const (
@ -108,13 +108,13 @@ type Channels struct {
}
// CreateChannels is the constructor for the Channels component
func CreateChannelsComponent(height int) *Channels {
func CreateChannelsComponent(inputHeight int) *Channels {
channels := &Channels{
List: termui.NewList(),
}
channels.List.BorderLabel = "Channels"
channels.List.Height = height
channels.List.Height = termui.TermHeight() - inputHeight
channels.SelectedChannel = 0
channels.Offset = 0
@ -148,16 +148,17 @@ func (c *Channels) Buffer() termui.Buffer {
// Append ellipsis when overflows
cells = termui.DTrimTxCls(cells, c.List.InnerWidth())
x := c.List.InnerBounds().Min.X
x := 0
for _, cell := range cells {
buf.Set(x, y, cell)
x += cell.Width()
width := cell.Width()
buf.Set(c.List.InnerBounds().Min.X+x, y, cell)
x += width
}
// When not at the end of the pane fill it up empty characters
for x < c.List.InnerBounds().Max.X {
for x < c.List.InnerBounds().Max.X-1 {
if y == c.CursorPosition {
buf.Set(x, y,
buf.Set(x+1, y,
termui.Cell{
Ch: ' ',
Fg: c.List.ItemBgColor,
@ -166,7 +167,7 @@ func (c *Channels) Buffer() termui.Buffer {
)
} else {
buf.Set(
x, y,
x+1, y,
termui.Cell{
Ch: ' ',
Fg: c.List.ItemFgColor,
@ -235,11 +236,6 @@ func (c *Channels) SetSelectedChannel(index int) {
c.SelectedChannel = index
}
// Get SelectedChannel returns the ChannelItem that is currently selected
func (c *Channels) GetSelectedChannel() ChannelItem {
return c.ChannelItems[c.SelectedChannel]
}
// MoveCursorUp will decrease the SelectedChannel by 1
func (c *Channels) MoveCursorUp() {
if c.SelectedChannel > 0 {
@ -325,14 +321,17 @@ func (c *Channels) Search(term string) {
}
if len(c.SearchMatches) > 0 {
c.GotoPositionSearch(0)
c.GotoPosition(0)
c.SearchPosition = 0
}
}
// GotoPosition is used by to automatically scroll to a specific
// location in the channels component
func (c *Channels) GotoPosition(newPos int) {
// GotoPosition is used by the search functionality to automatically
// scroll to a specific location in the channels component
func (c *Channels) GotoPosition(position int) {
// The new position
newPos := c.SearchMatches[position]
// Is the new position in range of the current view?
minRange := c.Offset
@ -359,18 +358,11 @@ func (c *Channels) GotoPosition(newPos int) {
c.CursorPosition = (newPos - c.Offset) + 1
}
// GotoPosition is used by the search functionality to automatically
// scroll to a specific location in the channels component
func (c *Channels) GotoPositionSearch(position int) {
newPos := c.SearchMatches[position]
c.GotoPosition(newPos)
}
// SearchNext allows us to cycle through the c.SearchMatches
func (c *Channels) SearchNext() {
newPosition := c.SearchPosition + 1
if newPosition <= len(c.SearchMatches)-1 {
c.GotoPositionSearch(newPosition)
c.GotoPosition(newPosition)
c.SearchPosition = newPosition
}
}
@ -379,17 +371,7 @@ func (c *Channels) SearchNext() {
func (c *Channels) SearchPrev() {
newPosition := c.SearchPosition - 1
if newPosition >= 0 {
c.GotoPositionSearch(newPosition)
c.GotoPosition(newPosition)
c.SearchPosition = newPosition
}
}
// Jump to the first channel with a notification
func (c *Channels) Jump() {
for i, channel := range c.ChannelItems {
if channel.Notification {
c.GotoPosition(i)
break
}
}
}

View File

@ -12,19 +12,58 @@ import (
"github.com/erroneousboat/slack-term/config"
)
var (
COLORS = []string{
"fg-black",
"fg-red",
"fg-green",
"fg-yellow",
"fg-blue",
"fg-magenta",
"fg-cyan",
"fg-white",
}
)
type Message struct {
Time time.Time
Name string
Content string
StyleTime string
StyleName string
StyleText string
FormatTime string
}
func (m Message) colorizeName(styleName string) string {
if strings.Contains(styleName, "colorize") {
var sum int
for _, c := range m.Name {
sum = sum + int(c)
}
i := sum % len(COLORS)
return strings.Replace(m.StyleName, "colorize", COLORS[i], -1)
}
return styleName
}
// Chat is the definition of a Chat component
type Chat struct {
List *termui.List
Messages map[string]Message
Messages []Message
Offset int
}
// CreateChatComponent is the constructor for the Chat struct
// CreateChat is the constructor for the Chat struct
func CreateChatComponent(inputHeight int) *Chat {
chat := &Chat{
List: termui.NewList(),
Messages: make(map[string]Message),
Offset: 0,
List: termui.NewList(),
Offset: 0,
}
chat.List.Height = termui.TermHeight() - inputHeight
@ -35,8 +74,59 @@ func CreateChatComponent(inputHeight int) *Chat {
// Buffer implements interface termui.Bufferer
func (c *Chat) Buffer() termui.Buffer {
// Convert Messages into termui.Cell
cells := c.MessagesToCells(c.Messages)
// Build cells. We're building parts of the message individually, or else
// DefaultTxBuilder will interpret potential markdown usage in a message
// as well.
cells := make([]termui.Cell, 0)
for i, msg := range c.Messages {
// When msg.Time and msg.Name are empty (in the case of attachments)
// don't add the time and name parts.
if (msg.Time != time.Time{} && msg.Name != "") {
// Time
cells = append(cells, termui.DefaultTxBuilder.Build(
fmt.Sprintf(
"[[%s]](%s) ",
msg.Time.Format(msg.FormatTime),
msg.StyleTime,
),
termui.ColorDefault, termui.ColorDefault)...,
)
// Name
cells = append(cells, termui.DefaultTxBuilder.Build(
fmt.Sprintf("[<%s>](%s) ",
msg.Name,
msg.colorizeName(msg.StyleName),
),
termui.ColorDefault, termui.ColorDefault)...,
)
}
// Hack, in order to get the correct fg and bg attributes. This is
// because the readAttr function in termui is unexported.
txCells := termui.DefaultTxBuilder.Build(
fmt.Sprintf("[.](%s)", msg.StyleText),
termui.ColorDefault, termui.ColorDefault,
)
// Text
for _, r := range msg.Content {
cells = append(
cells,
termui.Cell{
Ch: r,
Fg: txCells[0].Fg,
Bg: txCells[0].Bg,
},
)
}
// Add a newline after every message
if i < len(c.Messages)-1 {
cells = append(cells, termui.Cell{Ch: '\n'})
}
}
// We will create an array of Line structs, this allows us
// to more easily render the items in a list. We will range
@ -172,43 +262,17 @@ func (c *Chat) SetMessages(messages []Message) {
// Reset offset first, when scrolling in view and changing channels we
// want the offset to be 0 when loading new messages
c.Offset = 0
for _, msg := range messages {
c.Messages[msg.ID] = msg
}
c.Messages = messages
}
// AddMessage adds a single message to Messages
func (c *Chat) AddMessage(message Message) {
c.Messages[message.ID] = message
}
// AddReply adds a single reply to a parent thread, it also sets
// the thread separator
func (c *Chat) AddReply(parentID string, message Message) {
// It is possible that a message is received but the parent is not
// present in the chat view
if _, ok := c.Messages[parentID]; ok {
message.Thread = " "
c.Messages[parentID].Messages[message.ID] = message
} else {
c.AddMessage(message)
}
}
// IsNewThread check whether a message that is going to be added as
// a child to a parent message, is the first one or not
func (c *Chat) IsNewThread(parentID string) bool {
if parent, ok := c.Messages[parentID]; ok {
if len(parent.Messages) > 0 {
return true
}
}
return false
c.Messages = append(c.Messages, message)
}
// ClearMessages clear the c.Messages
func (c *Chat) ClearMessages() {
c.Messages = make(map[string]Message)
c.Messages = make([]Message, 0)
}
// ScrollUp will render the chat messages based on the Offset of the Chat
@ -248,101 +312,23 @@ func (c *Chat) SetBorderLabel(channelName string) {
c.List.BorderLabel = channelName
}
// MessagesToCells is a wrapper around MessageToCells to use for a slice of
// of type Message
func (c *Chat) MessagesToCells(msgs map[string]Message) []termui.Cell {
cells := make([]termui.Cell, 0)
sortedMessages := SortMessages(msgs)
for i, msg := range sortedMessages {
cells = append(cells, c.MessageToCells(msg)...)
if len(msg.Messages) > 0 {
cells = append(cells, termui.Cell{Ch: '\n'})
cells = append(cells, c.MessagesToCells(msg.Messages)...)
}
// Add a newline after every message
if i < len(sortedMessages)-1 {
cells = append(cells, termui.Cell{Ch: '\n'})
}
}
return cells
}
// MessageToCells will convert a Message struct to termui.Cell
//
// We're building parts of the message individually, or else DefaultTxBuilder
// will interpret potential markdown usage in a message as well.
func (c *Chat) MessageToCells(msg Message) []termui.Cell {
cells := make([]termui.Cell, 0)
// When msg.Time and msg.Name are empty (in the case of attachments)
// don't add the time and name parts.
if (msg.Time != time.Time{} && msg.Name != "") {
// Time
cells = append(cells, termui.DefaultTxBuilder.Build(
msg.GetTime(),
termui.ColorDefault, termui.ColorDefault)...,
)
// Thread
cells = append(cells, termui.DefaultTxBuilder.Build(
msg.GetThread(),
termui.ColorDefault, termui.ColorDefault)...,
)
// Name
cells = append(cells, termui.DefaultTxBuilder.Build(
msg.GetName(),
termui.ColorDefault, termui.ColorDefault)...,
)
}
// Hack, in order to get the correct fg and bg attributes. This is
// because the readAttr function in termui is unexported.
txCells := termui.DefaultTxBuilder.Build(
msg.GetContent(),
termui.ColorDefault, termui.ColorDefault,
)
// Text
for _, r := range msg.Content {
cells = append(
cells,
termui.Cell{
Ch: r,
Fg: txCells[0].Fg,
Bg: txCells[0].Bg,
},
)
}
return cells
}
// Help shows the usage and key bindings in the chat pane
func (c *Chat) Help(usage string, cfg *config.Config) {
msgUsage := Message{
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
Content: usage,
help := []Message{
Message{
Content: usage,
},
}
c.Messages[msgUsage.ID] = msgUsage
for mode, mapping := range cfg.KeyMap {
msgMode := Message{
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
Content: fmt.Sprintf("%s", strings.ToUpper(mode)),
}
c.Messages[msgMode.ID] = msgMode
help = append(
help,
Message{
Content: fmt.Sprintf("%s", strings.ToUpper(mode)),
},
)
msgNewline := Message{
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
Content: "",
}
c.Messages[msgNewline.ID] = msgNewline
help = append(help, Message{Content: ""})
var keys []string
for k := range mapping {
@ -351,14 +337,16 @@ func (c *Chat) Help(usage string, cfg *config.Config) {
sort.Strings(keys)
for _, k := range keys {
msgKey := Message{
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
Content: fmt.Sprintf(" %-12s%-15s", k, mapping[k]),
}
c.Messages[msgKey.ID] = msgKey
help = append(
help,
Message{
Content: fmt.Sprintf(" %-12s%-15s", k, mapping[k]),
},
)
}
msgNewline.ID = fmt.Sprintf("%d", time.Now().UnixNano())
c.Messages[msgNewline.ID] = msgNewline
help = append(help, Message{Content: ""})
}
c.Messages = help
}

View File

@ -1,10 +1,6 @@
package components
import (
"fmt"
"github.com/erroneousboat/termui"
)
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
@ -20,7 +16,6 @@ func CreateDebugComponent(inputHeight int) *Debug {
debug.List.BorderLabel = "Debug"
debug.List.Height = termui.TermHeight() - inputHeight
debug.List.Overflow = "wrap"
return debug
}
@ -61,15 +56,3 @@ func (d *Debug) Println(text string) {
termui.Render(d)
}
func (d *Debug) Sprintf(format string, a ...interface{}) {
text := fmt.Sprintf(format, a...)
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

@ -85,18 +85,7 @@ func (i *Input) Insert(key rune) {
// Backspace will remove a character in front of the CursorPositionText
func (i *Input) Backspace() {
if i.CursorPositionText > 0 {
// We want the cursor to stay in the same spot when the text
// overflow, revealing the test on the left side when using
// backspace. When all the text has been revealed will move
// the cursor to the left.
if i.Offset > 0 {
i.Offset--
i.CursorPositionText--
} else {
i.MoveCursorLeft()
}
i.MoveCursorLeft()
i.Text = append(i.Text[0:i.CursorPositionText], i.Text[i.CursorPositionText+1:]...)
i.Par.Text = string(i.Text[i.Offset:])
}

View File

@ -1,95 +0,0 @@
package components
import (
"fmt"
"sort"
"strings"
"time"
)
var (
COLORS = []string{
"fg-black",
"fg-red",
"fg-green",
"fg-yellow",
"fg-blue",
"fg-magenta",
"fg-cyan",
"fg-white",
}
)
type Message struct {
ID string
Messages map[string]Message
Time time.Time
Thread string
Name string
Content string
StyleTime string
StyleThread string
StyleName string
StyleText string
FormatTime string
}
func (m Message) GetTime() string {
return fmt.Sprintf(
"[[%s]](%s) ",
m.Time.Format(m.FormatTime),
m.StyleTime,
)
}
func (m Message) GetThread() string {
return fmt.Sprintf("[%s](%s)",
m.Thread,
m.StyleThread,
)
}
func (m Message) GetName() string {
return fmt.Sprintf("[<%s>](%s) ",
m.Name,
m.colorizeName(m.StyleName),
)
}
func (m Message) GetContent() string {
return fmt.Sprintf("[.](%s)", m.StyleText)
}
func (m Message) colorizeName(styleName string) string {
if strings.Contains(styleName, "colorize") {
var sum int
for _, c := range m.Name {
sum = sum + int(c)
}
i := sum % len(COLORS)
return strings.Replace(m.StyleName, "colorize", COLORS[i], -1)
}
return styleName
}
func SortMessages(msgs map[string]Message) []Message {
keys := make([]string, 0)
for k := range msgs {
keys = append(keys, k)
}
sort.Strings(keys)
sortedMessages := make([]Message, 0)
for _, k := range keys {
sortedMessages = append(sortedMessages, msgs[k])
}
return sortedMessages
}

View File

@ -1,26 +0,0 @@
package components
import (
"github.com/erroneousboat/termui"
)
type Threads struct {
*Channels
}
func CreateThreadsComponent(height int) *Threads {
threads := &Threads{
Channels: &Channels{
List: termui.NewList(),
},
}
threads.List.BorderLabel = "Threads"
threads.List.Height = height
threads.SelectedChannel = 0
threads.Offset = 0
threads.CursorPosition = threads.List.InnerBounds().Min.Y
return threads
}

View File

@ -4,11 +4,8 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
fp "path/filepath"
"github.com/OpenPeeDeeP/xdg"
"github.com/erroneousboat/termui"
)
@ -24,7 +21,6 @@ type Config struct {
Emoji bool `json:"emoji"`
SidebarWidth int `json:"sidebar_width"`
MainWidth int `json:"-"`
ThreadsWidth int `json:"threads_width"`
KeyMap map[string]keyMapping `json:"key_map"`
Theme Theme `json:"theme"`
}
@ -35,18 +31,13 @@ type keyMapping map[string]string
func NewConfig(filepath string) (*Config, error) {
cfg := getDefaultConfig()
// Open config file, and when none is found or present create
// a default empty one, at the default filepath location
file, err := os.Open(filepath)
if err != nil {
file, err = CreateConfigFile(filepath)
if err != nil {
return &cfg, fmt.Errorf("couldn't open the slack-term config file: (%v)", err)
}
return &cfg, fmt.Errorf("couldn't find the slack-term config file: %v", err)
}
if err := json.NewDecoder(file).Decode(&cfg); err != nil {
return &cfg, fmt.Errorf("the slack-term config file isn't valid json: (%v)", err)
return &cfg, fmt.Errorf("the slack-term config file isn't valid json: %v", err)
}
if cfg.SidebarWidth < 1 || cfg.SidebarWidth > 11 {
@ -74,32 +65,10 @@ func NewConfig(filepath string) (*Config, error) {
return &cfg, nil
}
func CreateConfigFile(filepath string) (*os.File, error) {
filepath = fmt.Sprintf("%s/slack-term/%s", xdg.ConfigHome(), "config")
if _, err := os.Stat(filepath); os.IsNotExist(err) {
os.MkdirAll(fp.Dir(filepath), os.ModePerm)
}
payload := "{\"slack_token\": \"\"}"
err := ioutil.WriteFile(filepath, []byte(payload), 0755)
if err != nil {
return nil, err
}
file, err := os.Open(filepath)
if err != nil {
return nil, err
}
return file, nil
}
func getDefaultConfig() Config {
return Config{
SidebarWidth: 1,
MainWidth: 11,
ThreadsWidth: 1,
Notify: "",
Emoji: false,
KeyMap: map[string]keyMapping{
@ -110,8 +79,6 @@ func getDefaultConfig() Config {
"j": "channel-down",
"g": "channel-top",
"G": "channel-bottom",
"K": "thread-up",
"J": "thread-down",
"<previous>": "chat-up",
"C-b": "chat-up",
"C-u": "chat-up",
@ -120,7 +87,6 @@ func getDefaultConfig() Config {
"C-d": "chat-down",
"n": "channel-search-next",
"N": "channel-search-prev",
"'": "channel-jump",
"q": "quit",
"<f1>": "help",
},
@ -162,7 +128,6 @@ func getDefaultConfig() Config {
Message: Message{
Time: "",
TimeFormat: "15:04",
Thread: "fg-bold",
Name: "",
Text: "",
},

View File

@ -18,7 +18,6 @@ type View struct {
type Message struct {
Time string `json:"time"`
Name string `json:"name"`
Thread string `json:"thread"`
Text string `json:"text"`
TimeFormat string `json:"time_format"`
}

View File

@ -19,9 +19,6 @@ const (
CommandMode = "command"
InsertMode = "insert"
SearchMode = "search"
ChatFocus = iota
ThreadFocus
)
type AppContext struct {
@ -34,7 +31,6 @@ type AppContext struct {
Config *config.Config
Debug bool
Mode string
Focus int
Notify *notificator.Notificator
}
@ -89,58 +85,32 @@ func CreateAppContext(flgConfig string, flgToken string, flgDebug bool, version
return nil, err
}
columns := []*termui.Row{
termui.NewCol(config.SidebarWidth, 0, view.Channels),
}
threads := false
if len(view.Threads.ChannelItems) > 0 {
threads = true
}
// Setup the interface
if threads && flgDebug {
columns = append(
columns,
[]*termui.Row{
termui.NewCol(config.MainWidth-config.ThreadsWidth-3, 0, view.Chat),
termui.NewCol(config.ThreadsWidth, 0, view.Threads),
termui.NewCol(3, 0, view.Debug),
}...,
)
} else if threads {
columns = append(
columns,
[]*termui.Row{
termui.NewCol(config.MainWidth-config.ThreadsWidth, 0, view.Chat),
termui.NewCol(config.ThreadsWidth, 0, view.Threads),
}...,
)
} else if flgDebug {
columns = append(
columns,
[]*termui.Row{
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 {
columns = append(
columns,
[]*termui.Row{
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.AddRows(
termui.NewRow(columns...),
termui.NewRow(
termui.NewCol(config.SidebarWidth, 0, view.Mode),
termui.NewCol(config.MainWidth, 0, view.Input),
),
)
termui.Body.Align()
termui.Render(termui.Body)
@ -154,7 +124,6 @@ func CreateAppContext(flgConfig string, flgToken string, flgDebug bool, version
Config: config,
Debug: flgDebug,
Mode: CommandMode,
Focus: ChatFocus,
Notify: notify,
}, nil
}

21
go.mod
View File

@ -1,21 +0,0 @@
module github.com/erroneousboat/slack-term
go 1.12
require (
github.com/0xAX/notificator v0.0.0-20171022182052-88d57ee9043b
github.com/OpenPeeDeeP/xdg v0.2.0
github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04
github.com/gorilla/websocket v1.4.2 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/lithammer/fuzzysearch v1.1.0
github.com/maruel/panicparse v1.1.1 // indirect
github.com/mattn/go-runewidth v0.0.7
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/nsf/termbox-go v0.0.0-20191229070316-58d4fcbce2a7
github.com/pkg/errors v0.9.1 // indirect
github.com/slack-go/slack v0.6.3
github.com/stretchr/testify v1.4.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.2.4 // indirect
)

53
go.sum
View File

@ -1,53 +0,0 @@
github.com/0xAX/notificator v0.0.0-20171022182052-88d57ee9043b h1:Sn+u6zpXFyfm2X7ruh+z6SJiUVyFg8YElh6HIOhrRCA=
github.com/0xAX/notificator v0.0.0-20171022182052-88d57ee9043b/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s=
github.com/OpenPeeDeeP/xdg v0.2.0 h1:xr89rnllbkRkM7SV9Y++FJ8TGkbdkhhBQm5kOkGT7AE=
github.com/OpenPeeDeeP/xdg v0.2.0/go.mod h1:tMoSueLQlMf0TCldjrJLNIjAc5qAOIcHt5REi88/Ygo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04 h1:DaFwoQC0Neeb2y2CVFxDPrS1BeyWAkKc4VVBDTZ0N98=
github.com/erroneousboat/termui v0.0.0-20170923115141-80f245cdfa04/go.mod h1:UPpsbgDrqmUayOFOkCGD7+xrBIml/1dA0dsqTRnZqac=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lithammer/fuzzysearch v1.1.0 h1:go9v8tLCrNTTlH42OAaq4eHFe81TDHEnlrMEb6R4f+A=
github.com/lithammer/fuzzysearch v1.1.0/go.mod h1:Bqx4wo8lTOFcJr3ckpY6HA9lEIOO0H5HrkJ5CsN56HQ=
github.com/maruel/panicparse v1.1.1 h1:k62YPcEoLncEEpjMt92GtG5ugb8WL/510Ys3/h5IkRc=
github.com/maruel/panicparse v1.1.1/go.mod h1:nty42YY5QByNC5MM7q/nj938VbgPU7avs45z6NClpxI=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/nlopes/slack v0.6.0 h1:jt0jxVQGhssx1Ib7naAOZEZcGdtIhTzkP0nopK0AsRA=
github.com/nlopes/slack v0.6.0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk=
github.com/nsf/termbox-go v0.0.0-20191229070316-58d4fcbce2a7 h1:OkWEy7aQeQTbgdrcGi9bifx+Y6bMM7ae7y42hDFaBvA=
github.com/nsf/termbox-go v0.0.0-20191229070316-58d4fcbce2a7/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/slack-go/slack v0.6.3 h1:qU037g8gQ71EuH6S9zYKnvYrEUj0fLFH4HFekFqBoRU=
github.com/slack-go/slack v0.6.3/go.mod h1:HE4RwNe7YpOg/F0vqo5PwXH3Hki31TplTvKRW9dGGaw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -11,8 +11,8 @@ import (
"github.com/0xAX/notificator"
"github.com/erroneousboat/termui"
"github.com/nlopes/slack"
termbox "github.com/nsf/termbox-go"
"github.com/slack-go/slack"
"github.com/erroneousboat/slack-term/components"
"github.com/erroneousboat/slack-term/config"
@ -44,16 +44,12 @@ var actionMap = map[string]func(*context.AppContext){
"channel-bottom": actionMoveCursorBottomChannels,
"channel-search-next": actionSearchNextChannels,
"channel-search-prev": actionSearchPrevChannels,
"channel-jump": actionJumpChannels,
"thread-up": actionMoveCursorUpThreads,
"thread-down": actionMoveCursorDownThreads,
"chat-up": actionScrollUpChat,
"chat-down": actionScrollDownChat,
"help": actionHelp,
}
// Initialize will start a combination of event handlers and 'background tasks'
func Initialize(ctx *context.AppContext) {
func RegisterEventHandlers(ctx *context.AppContext) {
// Keyboard events
eventHandler(ctx)
@ -119,12 +115,12 @@ func messageHandler(ctx *context.AppContext) {
go func() {
for {
select {
case rtmEvent := <-ctx.Service.RTM.IncomingEvents:
switch ev := rtmEvent.Data.(type) {
case msg := <-ctx.Service.RTM.IncomingEvents:
switch ev := msg.Data.(type) {
case *slack.MessageEvent:
// Construct message
msg, err := ctx.Service.CreateMessageFromMessageEvent(ev, ev.Channel)
msg, err := ctx.Service.CreateMessageFromMessageEvent(ev)
if err != nil {
continue
}
@ -132,33 +128,15 @@ func messageHandler(ctx *context.AppContext) {
// Add message to the selected channel
if ev.Channel == ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID {
// Get the thread timestamp of the event, we need to
// check the previous message as well, because edited
// message don't have the thread timestamp
var threadTimestamp string
if ev.ThreadTimestamp != "" {
threadTimestamp = ev.ThreadTimestamp
} else if ev.PreviousMessage != nil && ev.PreviousMessage.ThreadTimestamp != "" {
threadTimestamp = ev.PreviousMessage.ThreadTimestamp
} else {
threadTimestamp = ""
// 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],
)
}
// When timestamp isn't set this is a thread reply,
// handle as such
if threadTimestamp != "" {
ctx.View.Chat.AddReply(threadTimestamp, msg)
} else if threadTimestamp == "" && ctx.Focus == context.ChatFocus {
ctx.View.Chat.AddMessage(msg)
}
// we (mis)use actionChangeChannel, to rerender, the
// view when a new thread has been started
if ctx.View.Chat.IsNewThread(threadTimestamp) {
actionChangeChannel(ctx)
} else {
termui.Render(ctx.View.Chat)
}
termui.Render(ctx.View.Chat)
// TODO: set Chat.Offset to 0, to automatically scroll
// down?
@ -225,64 +203,6 @@ func actionResizeEvent(ctx *context.AppContext, ev termbox.Event) {
termui.Render(termui.Body)
}
func actionRedrawGrid(ctx *context.AppContext, threads bool, debug bool) {
termui.Clear()
termui.Body = termui.NewGrid()
termui.Body.X = 0
termui.Body.Y = 0
termui.Body.BgColor = termui.ThemeAttr("bg")
termui.Body.Width = termui.TermWidth()
columns := []*termui.Row{
termui.NewCol(ctx.Config.SidebarWidth, 0, ctx.View.Channels),
}
if threads && debug {
columns = append(
columns,
[]*termui.Row{
termui.NewCol(ctx.Config.MainWidth-ctx.Config.ThreadsWidth-3, 0, ctx.View.Chat),
termui.NewCol(ctx.Config.ThreadsWidth, 0, ctx.View.Threads),
termui.NewCol(3, 0, ctx.View.Debug),
}...,
)
} else if threads {
columns = append(
columns,
[]*termui.Row{
termui.NewCol(ctx.Config.MainWidth-ctx.Config.ThreadsWidth, 0, ctx.View.Chat),
termui.NewCol(ctx.Config.ThreadsWidth, 0, ctx.View.Threads),
}...,
)
} else if debug {
columns = append(
columns,
[]*termui.Row{
termui.NewCol(ctx.Config.MainWidth-5, 0, ctx.View.Chat),
termui.NewCol(ctx.Config.MainWidth-6, 0, ctx.View.Debug),
}...,
)
} else {
columns = append(
columns,
[]*termui.Row{
termui.NewCol(ctx.Config.MainWidth, 0, ctx.View.Chat),
}...,
)
}
termui.Body.AddRows(
termui.NewRow(columns...),
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)
}
func actionInput(view *views.View, key rune) {
view.Input.Insert(key)
termui.Render(view.Input)
@ -328,10 +248,10 @@ func actionSend(ctx *context.AppContext) {
// quick succession of actionSend
message := ctx.View.Input.GetText()
ctx.View.Input.Clear()
termui.Render(ctx.View.Input)
ctx.View.Refresh()
// Send slash command
isCmd, err := ctx.Service.SendCommand(
// Send message
err := ctx.Service.SendMessage(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
message,
)
@ -341,39 +261,10 @@ func actionSend(ctx *context.AppContext) {
)
}
// Send message
if !isCmd {
if ctx.Focus == context.ChatFocus {
err := ctx.Service.SendMessage(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
message,
)
if err != nil {
ctx.View.Debug.Println(
err.Error(),
)
}
}
if ctx.Focus == context.ThreadFocus {
err := ctx.Service.SendReply(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
ctx.View.Threads.ChannelItems[ctx.View.Threads.SelectedChannel].ID,
message,
)
if err != nil {
ctx.View.Debug.Println(
err.Error(),
)
}
}
}
// Clear notification icon if there is any
channelItem := ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel]
if channelItem.Notification {
ctx.Service.MarkAsRead(channelItem)
ctx.Service.MarkAsRead(channelItem.ID)
ctx.View.Channels.MarkAsRead(ctx.View.Channels.SelectedChannel)
}
termui.Render(ctx.View.Channels)
@ -426,7 +317,7 @@ func actionSearchMode(ctx *context.AppContext) {
}
func actionGetMessages(ctx *context.AppContext) {
msgs, _, err := ctx.Service.GetMessages(
msgs, err := ctx.Service.GetMessages(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
ctx.View.Chat.GetMaxItems(),
)
@ -501,18 +392,13 @@ func actionSearchPrevChannels(ctx *context.AppContext) {
actionChangeChannel(ctx)
}
func actionJumpChannels(ctx *context.AppContext) {
ctx.View.Channels.Jump()
actionChangeChannel(ctx)
}
func actionChangeChannel(ctx *context.AppContext) {
// Clear messages from Chat pane
ctx.View.Chat.ClearMessages()
// Get messages of the SelectedChannel, and get the count of messages
// that fit into the Chat component
msgs, threads, err := ctx.Service.GetMessages(
msgs, err := ctx.Service.GetMessages(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
ctx.View.Chat.GetMaxItems(),
)
@ -525,23 +411,6 @@ func actionChangeChannel(ctx *context.AppContext) {
// Set messages for the channel
ctx.View.Chat.SetMessages(msgs)
// Set the threads identifiers in the threads pane
var haveThreads bool
if len(threads) > 0 {
haveThreads = true
// Make the first thread the current Channel
ctx.View.Threads.SetChannels(
append(
[]components.ChannelItem{ctx.View.Channels.GetSelectedChannel()},
threads...,
),
)
// Reset position of SelectedChannel
ctx.View.Threads.MoveCursorTop()
}
// Set channel name for the Chat pane
ctx.View.Chat.SetBorderLabel(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].GetChannelName(),
@ -550,105 +419,14 @@ func actionChangeChannel(ctx *context.AppContext) {
// Clear notification icon if there is any
channelItem := ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel]
if channelItem.Notification {
ctx.Service.MarkAsRead(channelItem)
ctx.Service.MarkAsRead(channelItem.ID)
ctx.View.Channels.MarkAsRead(ctx.View.Channels.SelectedChannel)
}
// Redraw grid, necessary when threads and/or debug is set. We will redraw
// the grid when there are threads, or we just came from a thread and went
// to a channel without threads. Hence the clearing of ChannelItems of
// Threads.
if haveThreads {
actionRedrawGrid(ctx, haveThreads, ctx.Debug)
} else if !haveThreads && len(ctx.View.Threads.ChannelItems) > 0 {
ctx.View.Threads.SetChannels([]components.ChannelItem{})
actionRedrawGrid(ctx, haveThreads, ctx.Debug)
} else {
termui.Render(ctx.View.Threads)
termui.Render(ctx.View.Channels)
termui.Render(ctx.View.Chat)
}
// Set focus, necessary to know when replying to thread or chat
ctx.Focus = context.ChatFocus
}
func actionChangeThread(ctx *context.AppContext) {
// Clear messages from Chat pane
ctx.View.Chat.ClearMessages()
// The first channel in the Thread list is current Channel. Set context
// Focus and messages accordingly.
var err error
msgs := []components.Message{}
if ctx.View.Threads.SelectedChannel == 0 {
ctx.Focus = context.ChatFocus
msgs, _, err = ctx.Service.GetMessages(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
ctx.View.Chat.GetMaxItems(),
)
if err != nil {
termbox.Close()
log.Println(err)
os.Exit(0)
}
} else {
ctx.Focus = context.ThreadFocus
msgs, err = ctx.Service.GetMessageByID(
ctx.View.Threads.ChannelItems[ctx.View.Threads.SelectedChannel].ID,
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
)
if err != nil {
termbox.Close()
log.Println(err)
os.Exit(0)
}
}
// Set messages for the channel
ctx.View.Chat.SetMessages(msgs)
termui.Render(ctx.View.Channels)
termui.Render(ctx.View.Threads)
termui.Render(ctx.View.Chat)
}
func actionMoveCursorUpThreads(ctx *context.AppContext) {
go func() {
if scrollTimer != nil {
scrollTimer.Stop()
}
ctx.View.Threads.MoveCursorUp()
termui.Render(ctx.View.Threads)
scrollTimer = time.NewTimer(time.Second / 4)
<-scrollTimer.C
// Only actually change channel when the timer expires
actionChangeThread(ctx)
}()
}
func actionMoveCursorDownThreads(ctx *context.AppContext) {
go func() {
if scrollTimer != nil {
scrollTimer.Stop()
}
ctx.View.Threads.MoveCursorDown()
termui.Render(ctx.View.Threads)
scrollTimer = time.NewTimer(time.Second / 4)
<-scrollTimer.C
// Only actually change thread when the timer expires
actionChangeThread(ctx)
}()
}
// actionNewMessage will set the new message indicator for a channel, and
// if configured will also display a desktop notification
func actionNewMessage(ctx *context.AppContext, ev *slack.MessageEvent) {
@ -702,7 +480,6 @@ func actionScrollDownChat(ctx *context.AppContext) {
}
func actionHelp(ctx *context.AppContext) {
ctx.View.Chat.ClearMessages()
ctx.View.Chat.Help(ctx.Usage, ctx.Config)
termui.Render(ctx.View.Chat)
}

19
main.go
View File

@ -5,8 +5,9 @@ import (
"fmt"
"log"
"os"
"os/user"
"path"
"github.com/OpenPeeDeeP/xdg"
"github.com/erroneousboat/termui"
termbox "github.com/nsf/termbox-go"
@ -15,7 +16,7 @@ import (
)
const (
VERSION = "master"
VERSION = "v0.4.1"
USAGE = `NAME:
slack-term - slack client for your terminal
@ -44,15 +45,17 @@ var (
)
func init() {
// Find the default config file
configFile := xdg.New("slack-term", "").QueryConfig("config")
// Get home dir for config file default
usr, err := user.Current()
if err != nil {
log.Fatal(err)
}
// Parse flags
flag.StringVar(
&flgConfig,
"config",
configFile,
path.Join(usr.HomeDir, ".slack-term"),
"location of config file",
)
@ -105,8 +108,8 @@ func main() {
os.Exit(0)
}
// Initialize handlers
handlers.Initialize(ctx)
// Register handlers
handlers.RegisterEventHandlers(ctx)
termui.Loop()
}

View File

@ -3,9 +3,6 @@ package service
import (
"errors"
"fmt"
"html"
"log"
"net/url"
"regexp"
"sort"
"strconv"
@ -13,7 +10,7 @@ import (
"sync"
"time"
"github.com/slack-go/slack"
"github.com/nlopes/slack"
"github.com/erroneousboat/slack-term/components"
"github.com/erroneousboat/slack-term/config"
@ -25,7 +22,6 @@ type SlackService struct {
RTM *slack.RTM
Conversations []slack.Channel
UserCache map[string]string
ThreadCache map[string]string
CurrentUserID string
CurrentUsername string
}
@ -34,10 +30,9 @@ type SlackService struct {
// the RTM and a Client
func NewSlackService(config *config.Config) (*SlackService, error) {
svc := &SlackService{
Config: config,
Client: slack.New(config.SlackToken),
UserCache: make(map[string]string),
ThreadCache: make(map[string]string),
Config: config,
Client: slack.New(config.SlackToken),
UserCache: make(map[string]string),
}
// Get user associated with token, mainly
@ -63,13 +58,12 @@ func NewSlackService(config *config.Config) (*SlackService, error) {
}
}
// Get name of current user, and set presence to active
// Get name of current user
currentUser, err := svc.Client.GetUserInfo(svc.CurrentUserID)
if err != nil {
svc.CurrentUsername = "slack-term"
}
svc.CurrentUsername = currentUser.Name
svc.SetUserAsActive()
return svc, nil
}
@ -145,10 +139,6 @@ func (s *SlackService) GetChannels() ([]components.ChannelItem, error) {
chanItem.Type = components.ChannelTypeChannel
if chn.UnreadCount > 0 {
chanItem.Notification = true
}
buckets[0][chn.ID] = &tempChan{
channelItem: chanItem,
slackChannel: chn,
@ -168,10 +158,6 @@ func (s *SlackService) GetChannels() ([]components.ChannelItem, error) {
chanItem.Type = components.ChannelTypeMpIM
if chn.UnreadCount > 0 {
chanItem.Notification = true
}
buckets[2][chn.ID] = &tempChan{
channelItem: chanItem,
slackChannel: chn,
@ -180,10 +166,6 @@ func (s *SlackService) GetChannels() ([]components.ChannelItem, error) {
chanItem.Type = components.ChannelTypeGroup
if chn.UnreadCount > 0 {
chanItem.Notification = true
}
buckets[1][chn.ID] = &tempChan{
channelItem: chanItem,
slackChannel: chn,
@ -205,10 +187,6 @@ func (s *SlackService) GetChannels() ([]components.ChannelItem, error) {
chanItem.Type = components.ChannelTypeIM
chanItem.Presence = "away"
if chn.UnreadCount > 0 {
chanItem.Notification = true
}
buckets[3][chn.User] = &tempChan{
channelItem: chanItem,
slackChannel: chn,
@ -260,51 +238,26 @@ func (s *SlackService) GetUserPresence(userID string) (string, error) {
return presence.Presence, nil
}
// Set current user presence to active
func (s *SlackService) SetUserAsActive() {
s.Client.SetUserPresence("auto")
}
// MarkAsRead will set the channel as read
func (s *SlackService) MarkAsRead(channelItem components.ChannelItem) {
switch channelItem.Type {
case components.ChannelTypeChannel:
s.Client.SetChannelReadMark(
channelItem.ID, fmt.Sprintf("%f",
float64(time.Now().Unix())),
)
case components.ChannelTypeGroup:
s.Client.SetGroupReadMark(
channelItem.ID, fmt.Sprintf("%f",
float64(time.Now().Unix())),
)
case components.ChannelTypeMpIM:
s.Client.MarkIMChannel(
channelItem.ID, fmt.Sprintf("%f",
float64(time.Now().Unix())),
)
case components.ChannelTypeIM:
s.Client.MarkIMChannel(
channelItem.ID, fmt.Sprintf("%f",
float64(time.Now().Unix())),
)
}
func (s *SlackService) MarkAsRead(channelID string) {
s.Client.SetChannelReadMark(
channelID, fmt.Sprintf("%f",
float64(time.Now().Unix())),
)
}
// SendMessage will send a message to a particular channel
func (s *SlackService) SendMessage(channelID string, message string) error {
// https://godoc.org/github.com/nlopes/slack#PostMessageParameters
postParams := slack.MsgOptionPostMessageParameters(slack.PostMessageParameters{
postParams := slack.PostMessageParameters{
AsUser: true,
Username: s.CurrentUsername,
LinkNames: 1,
})
text := slack.MsgOptionText(message, true)
}
// https://godoc.org/github.com/nlopes/slack#Client.PostMessage
_, _, err := s.Client.PostMessage(channelID, text, postParams)
_, _, err := s.Client.PostMessage(channelID, message, postParams)
if err != nil {
return err
}
@ -312,103 +265,9 @@ func (s *SlackService) SendMessage(channelID string, message string) error {
return nil
}
// SendReply will send a message to a particular thread, specifying the
// ThreadTimestamp will make it reply to that specific thread. (see:
// https://api.slack.com/docs/message-threading, 'Posting replies')
func (s *SlackService) SendReply(channelID string, threadID string, message string) error {
// https://godoc.org/github.com/nlopes/slack#PostMessageParameters
postParams := slack.MsgOptionPostMessageParameters(slack.PostMessageParameters{
AsUser: true,
Username: s.CurrentUsername,
LinkNames: 1,
ThreadTimestamp: threadID,
})
text := slack.MsgOptionText(message, true)
// https://godoc.org/github.com/nlopes/slack#Client.PostMessage
_, _, err := s.Client.PostMessage(channelID, text, postParams)
if err != nil {
return err
}
return nil
}
// SendCommand will send a specific command to slack. First we check
// wether we are dealing with a command, and if it is one of the supported
// ones.
//
// NOTE: slack slash commands that are sent to the slack api are undocumented,
// and as such we need to update the message option that direct it to the
// correct api endpoint.
//
// https://github.com/ErikKalkoken/slackApiDoc/blob/master/chat.command.md
func (s *SlackService) SendCommand(channelID string, message string) (bool, error) {
// First check if it begins with slash and a command
r, err := regexp.Compile(`^/\w+`)
if err != nil {
return false, err
}
match := r.MatchString(message)
if !match {
return false, nil
}
// Execute the the command when supported
switch r.FindString(message) {
case "/thread":
r := regexp.MustCompile(`(?P<cmd>^/\w+) (?P<id>\w+) (?P<msg>.*)`)
subMatch := r.FindStringSubmatch(message)
if len(subMatch) < 4 {
return false, errors.New("'/thread' command malformed")
}
threadID := s.ThreadCache[subMatch[2]]
msg := subMatch[3]
err := s.SendReply(channelID, threadID, msg)
if err != nil {
return false, err
}
return true, nil
default:
r := regexp.MustCompile(`(?P<cmd>^/\w+) (?P<text>.*)`)
subMatch := r.FindStringSubmatch(message)
if len(subMatch) < 3 {
return false, errors.New("slash command malformed")
}
cmd := subMatch[1]
text := subMatch[2]
msgOption := slack.UnsafeMsgOptionEndpoint(
fmt.Sprintf("%s%s", slack.APIURL, "chat.command"),
func(urlValues url.Values) {
urlValues.Add("command", cmd)
urlValues.Add("text", text)
},
)
_, _, err := s.Client.PostMessage(channelID, msgOption)
if err != nil {
return false, err
}
return true, nil
}
return false, nil
}
// GetMessages will get messages for a channel, group or im channel delimited
// by a count. It will return the messages, the thread identifiers
// (as ChannelItem), and and error.
func (s *SlackService) GetMessages(channelID string, count int) ([]components.Message, []components.ChannelItem, error) {
// by a count.
func (s *SlackService) GetMessages(channelID string, count int) ([]components.Message, error) {
// https://godoc.org/github.com/nlopes/slack#GetConversationHistoryParameters
historyParams := slack.GetConversationHistoryParameters{
@ -419,27 +278,14 @@ func (s *SlackService) GetMessages(channelID string, count int) ([]components.Me
history, err := s.Client.GetConversationHistory(&historyParams)
if err != nil {
return nil, nil, err
return nil, err
}
// Construct the messages
var messages []components.Message
var threads []components.ChannelItem
for _, message := range history.Messages {
msg := s.CreateMessage(message, channelID)
messages = append(messages, msg)
// FIXME: create boolean isThread
if msg.Thread != "" {
threads = append(threads, components.ChannelItem{
ID: msg.ID,
Name: msg.Thread,
Type: components.ChannelTypeGroup,
StylePrefix: s.Config.Theme.Channel.Prefix,
StyleIcon: s.Config.Theme.Channel.Icon,
StyleText: s.Config.Theme.Channel.Text,
})
}
msg := s.CreateMessage(message)
messages = append(messages, msg...)
}
// Reverse the order of the messages, we want the newest in
@ -449,45 +295,18 @@ func (s *SlackService) GetMessages(channelID string, count int) ([]components.Me
messagesReversed = append(messagesReversed, messages[i])
}
return messagesReversed, threads, nil
}
// CreateMessageByID will construct an array of components.Message with only
// 1 message, using the message ID (Timestamp).
//
// For the choice of history parameters see:
// https://api.slack.com/messaging/retrieving
func (s *SlackService) GetMessageByID(messageID string, channelID string) ([]components.Message, error) {
var msgs []components.Message
// https://godoc.org/github.com/nlopes/slack#GetConversationHistoryParameters
historyParams := slack.GetConversationHistoryParameters{
ChannelID: channelID,
Limit: 1,
Inclusive: true,
Latest: messageID,
}
history, err := s.Client.GetConversationHistory(&historyParams)
if err != nil {
return msgs, err
}
// We break because we're only asking for 1 message
for _, message := range history.Messages {
msgs = append(msgs, s.CreateMessage(message, channelID))
break
}
return msgs, nil
return messagesReversed, nil
}
// CreateMessage will create a string formatted message that can be rendered
// in the Chat pane.
//
// [23:59] <erroneousboat> Hello world!
func (s *SlackService) CreateMessage(message slack.Message, channelID string) components.Message {
//
// This returns an array of string because we will try to uncover attachments
// associated with messages.
func (s *SlackService) CreateMessage(message slack.Message) []components.Message {
var msgs []components.Message
var name string
// Get username from cache
@ -496,21 +315,12 @@ func (s *SlackService) CreateMessage(message slack.Message, channelID string) co
// Name not in cache
if !ok {
if message.BotID != "" {
// Name not found, perhaps a bot, use Username
name, ok = s.UserCache[message.BotID]
if !ok {
if message.Username != "" {
name = message.Username
s.UserCache[message.BotID] = message.Username
} else {
bot, err := s.Client.GetBotInfo(message.BotID)
if err != nil {
name = "unkown"
s.UserCache[message.BotID] = name
} else {
name = bot.Name
s.UserCache[message.BotID] = bot.Name
}
}
// Not found in cache, add it
name = message.Username
s.UserCache[message.BotID] = message.Username
}
} else {
// Not a bot, not in cache, get user info
@ -529,6 +339,11 @@ func (s *SlackService) CreateMessage(message slack.Message, channelID string) co
name = "unknown"
}
// When there are attachments append them
if len(message.Attachments) > 0 {
msgs = append(msgs, s.CreateMessageFromAttachments(message.Attachments)...)
}
// Parse time
floatTime, err := strconv.ParseFloat(message.Timestamp, 64)
if err != nil {
@ -538,240 +353,96 @@ func (s *SlackService) CreateMessage(message slack.Message, channelID string) co
// Format message
msg := components.Message{
ID: message.Timestamp,
Messages: make(map[string]components.Message),
Time: time.Unix(intTime, 0),
Name: name,
Content: parseMessage(s, message.Text),
StyleTime: s.Config.Theme.Message.Time,
StyleThread: s.Config.Theme.Message.Thread,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
Time: time.Unix(intTime, 0),
Name: name,
Content: parseMessage(s, message.Text),
StyleTime: s.Config.Theme.Message.Time,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
}
// When there are attachments, add them to Messages
//
// NOTE: attachments don't have an id or a timestamp that we can
// use as a key value for the Messages field, so we use the index
// of the returned array.
if len(message.Attachments) > 0 {
atts := s.CreateMessageFromAttachments(message.Attachments)
for i, a := range atts {
msg.Messages[strconv.Itoa(i)] = a
}
}
// When there are files, add them to Messages
if len(message.Files) > 0 {
files := s.CreateMessageFromFiles(message.Files)
for _, file := range files {
msg.Messages[file.ID] = file
}
}
// When the message timestamp and thread timestamp are the same, we
// have a parent message. This means it contains a thread with replies.
//
// Additionally, we set the thread timestamp in the s.ThreadCache with
// the base62 representation of the timestamp. We do this because
// we if we want to reply to a thread, we need to reference this
// timestamp. Which is too long to type, we shorten it and remember the
// reference in the cache.
if message.ThreadTimestamp != "" && message.ThreadTimestamp == message.Timestamp {
// Set the thread identifier for thread cache
f, _ := strconv.ParseFloat(message.ThreadTimestamp, 64)
threadID := hashID(int(f))
s.ThreadCache[threadID] = message.ThreadTimestamp
// Set thread prefix for message
msg.Thread = fmt.Sprintf("%s ", threadID)
// Create the message replies from the thread
replies := s.CreateMessageFromReplies(message.ThreadTimestamp, channelID)
for _, reply := range replies {
msg.Messages[reply.ID] = reply
}
}
return msg
}
// CreateMessageFromReplies will create components.Message struct from
// the conversation replies from slack.
//
// Useful documentation:
//
// https://api.slack.com/docs/message-threading
// https://api.slack.com/methods/conversations.replies
// https://godoc.org/github.com/nlopes/slack#Client.GetConversationReplies
// https://godoc.org/github.com/nlopes/slack#GetConversationRepliesParameters
func (s *SlackService) CreateMessageFromReplies(messageID string, channelID string) []components.Message {
msgs := make([]slack.Message, 0)
initReplies, _, initCur, err := s.Client.GetConversationReplies(
&slack.GetConversationRepliesParameters{
ChannelID: channelID,
Timestamp: messageID,
Limit: 200,
},
)
if err != nil {
log.Fatal(err) // FIXME
}
msgs = append(msgs, initReplies...)
nextCur := initCur
for nextCur != "" {
conversationReplies, _, cursor, err := s.Client.GetConversationReplies(&slack.GetConversationRepliesParameters{
ChannelID: channelID,
Timestamp: messageID,
Cursor: nextCur,
Limit: 200,
})
if err != nil {
log.Fatal(err) // FIXME
}
msgs = append(msgs, conversationReplies...)
nextCur = cursor
}
var replies []components.Message
for _, reply := range msgs {
// Because the conversations api returns an entire thread (a
// message plus all the messages in reply), we need to check if
// one of the replies isn't the parent that we started with.
//
// Keep in mind that the api returns the replies with the latest
// as the first element.
if reply.ThreadTimestamp != "" && reply.ThreadTimestamp == reply.Timestamp {
continue
}
msg := s.CreateMessage(reply, channelID)
// Set the thread separator
msg.Thread = " "
replies = append(replies, msg)
}
return replies
}
// CreateMessageFromAttachments will construct an array of strings from the
// Field values of Attachments of a Message.
func (s *SlackService) CreateMessageFromAttachments(atts []slack.Attachment) []components.Message {
var msgs []components.Message
for _, att := range atts {
for _, field := range att.Fields {
msgs = append(msgs, components.Message{
Content: fmt.Sprintf(
"%s %s",
field.Title,
field.Value,
),
StyleTime: s.Config.Theme.Message.Time,
StyleThread: s.Config.Theme.Message.Thread,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
},
)
}
if att.Pretext != "" {
msgs = append(
msgs,
components.Message{
Content: fmt.Sprintf("%s", att.Pretext),
StyleTime: s.Config.Theme.Message.Time,
StyleThread: s.Config.Theme.Message.Thread,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
},
)
}
if att.Text != "" {
msgs = append(
msgs,
components.Message{
Content: fmt.Sprintf("%s", att.Text),
StyleTime: s.Config.Theme.Message.Time,
StyleThread: s.Config.Theme.Message.Thread,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
},
)
}
if att.Title != "" {
msgs = append(
msgs,
components.Message{
Content: fmt.Sprintf("%s", att.Title),
StyleTime: s.Config.Theme.Message.Time,
StyleThread: s.Config.Theme.Message.Thread,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
},
)
}
}
msgs = append(msgs, msg)
return msgs
}
// CreateMessageFromFiles will create components.Message struct from
// conversation attached files
func (s *SlackService) CreateMessageFromFiles(files []slack.File) []components.Message {
func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent) ([]components.Message, error) {
var msgs []components.Message
for _, file := range files {
msgs = append(msgs, components.Message{
Content: fmt.Sprintf(
"%s %s", file.Title, file.URLPrivate,
),
StyleTime: s.Config.Theme.Message.Time,
StyleThread: s.Config.Theme.Message.Thread,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
})
}
return msgs
}
func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent, channelID string) (components.Message, error) {
msg := slack.Message{Msg: message.Msg}
var name string
switch message.SubType {
case "message_changed":
// Append (edited) when an edited message is received
msg = slack.Message{Msg: *message.SubMessage}
msg.Text = fmt.Sprintf("%s (edited)", msg.Text)
message = &slack.MessageEvent{Msg: *message.SubMessage}
message.Text = fmt.Sprintf("%s (edited)", message.Text)
case "message_replied":
return components.Message{}, errors.New("ignoring reply events")
// Ignore reply events
return nil, errors.New("ignoring reply events")
}
return s.CreateMessage(msg, channelID), nil
// Get username from cache
name, ok := s.UserCache[message.User]
// Name not in cache
if !ok {
if message.BotID != "" {
// Name not found, perhaps a bot, use Username
name, ok = s.UserCache[message.BotID]
if !ok {
// Not found in cache, add it
name = message.Username
s.UserCache[message.BotID] = message.Username
}
} else {
// Not a bot, not in cache, get user info
user, err := s.Client.GetUserInfo(message.User)
if err != nil {
name = "unknown"
s.UserCache[message.User] = name
} else {
name = user.Name
s.UserCache[message.User] = user.Name
}
}
}
if name == "" {
name = "unknown"
}
// When there are attachments append them
if len(message.Attachments) > 0 {
msgs = append(msgs, s.CreateMessageFromAttachments(message.Attachments)...)
}
// Parse time
floatTime, err := strconv.ParseFloat(message.Timestamp, 64)
if err != nil {
floatTime = 0.0
}
intTime := int64(floatTime)
// Format message
msg := components.Message{
Time: time.Unix(intTime, 0),
Name: name,
Content: parseMessage(s, message.Text),
StyleTime: s.Config.Theme.Message.Time,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
}
msgs = append(msgs, msg)
return msgs, nil
}
// parseMessage will parse a message string and find and replace:
// - emoji's
// - mentions
// - html unescape
func parseMessage(s *SlackService, msg string) string {
if s.Config.Emoji {
msg = parseEmoji(msg)
@ -779,8 +450,6 @@ func parseMessage(s *SlackService, msg string) string {
msg = parseMentions(s, msg)
msg = html.UnescapeString(msg)
return msg
}
@ -845,6 +514,56 @@ func parseEmoji(msg string) string {
)
}
// CreateMessageFromAttachments will construct a array of string of the Field
// values of Attachments from a Message.
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, 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,
FormatTime: s.Config.Theme.Message.TimeFormat,
},
)
}
if att.Text != "" {
msgs = append(
msgs,
components.Message{
Content: fmt.Sprintf("%s", att.Text),
StyleTime: s.Config.Theme.Message.Time,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
},
)
}
if att.Title != "" {
msgs = append(
msgs,
components.Message{
Content: fmt.Sprintf("%s", att.Title),
StyleTime: s.Config.Theme.Message.Time,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
},
)
}
}
return msgs
}
func (s *SlackService) createChannelItem(chn slack.Channel) components.ChannelItem {
return components.ChannelItem{
ID: chn.ID,
@ -856,15 +575,3 @@ func (s *SlackService) createChannelItem(chn slack.Channel) components.ChannelIt
StyleText: s.Config.Theme.Channel.Text,
}
}
func hashID(input int) string {
const base62Alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
hash := ""
for input > 0 {
hash = string(base62Alphabet[input%62]) + hash
input = int(input / 62)
}
return hash
}

28
snapcraft.yaml Normal file
View File

@ -0,0 +1,28 @@
name: slack-term
version: git
summary: Slack client for your terminal
description: |
A Slack client for your terminal.
* Get a slack token from https://api.slack.com/docs/oauth-test-tokens
* Create $HOME/snap/slack-term/current/slack-term.json
* Contents detailed at https://github.com/erroneousboat/slack-term
* slack-term --config $HOME/snap/slack-term/current/slack-term.json
grade: stable
confinement: strict
apps:
slack-term:
command: slack-term
plugs:
- network
- home
parts:
go:
source-tag: go1.7.5
slack-term:
after: [go]
source: .
plugin: go
go-importpath: github.com/erroneousboat/slack-term

View File

@ -1,3 +0,0 @@
*.test
*.out
.DS_STORE

View File

@ -1,12 +0,0 @@
language: go
go:
- 1.11.x
os:
- linux
- osx
before_install:
- go get -t -v ./...
script:
- go test -v -race -covermode=atomic -coverprofile=coverage.txt
after_success:
- bash <(curl -s https://codecov.io/bash)

View File

@ -1,29 +0,0 @@
BSD 3-Clause License
Copyright (c) 2017, OpenPeeDeeP
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,25 +0,0 @@
# XDG [![Build status](https://ci.appveyor.com/api/projects/status/9eoupq9jgsu2p0jv?svg=true)](https://ci.appveyor.com/project/dixonwille/xdg) [![Build Status](https://travis-ci.org/OpenPeeDeeP/xdg.svg?branch=master)](https://travis-ci.org/OpenPeeDeeP/xdg) [![Go Report Card](https://goreportcard.com/badge/github.com/OpenPeeDeeP/xdg)](https://goreportcard.com/report/github.com/OpenPeeDeeP/xdg) [![GoDoc](https://godoc.org/github.com/OpenPeeDeeP/xdg?status.svg)](https://godoc.org/github.com/OpenPeeDeeP/xdg) [![codecov](https://codecov.io/gh/OpenPeeDeeP/xdg/branch/master/graph/badge.svg)](https://codecov.io/gh/OpenPeeDeeP/xdg)
A cross platform package that tries to follow [XDG Standard](https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html) when possible. Since XDG is linux specific, I am only able to follow standards to the T on linux. But for the other operating systems I am finding similar best practice locations for the files.
## Locations Per OS
The following table shows what is used if the envrionment variable is not set. If the variable is set then this package uses that. Linux follows the default standards. Mac does when it comes to the home directory but for system wide it uses the standard `/Library/Application Support`. As for Windows, the variable defaults are just other environment variables set up by the operation system.
> When creating `XDG` application the `Vendor` and `Application` names are appeneded to the end of the path to keep projects unique.
| | Linux | Mac | Windows |
| ---: | :---: | :---: | :---: |
| `XDG_DATA_DIRS` | [`/usr/local/share`, `/usr/share`] | [`/Library/Application Support`] | `%PROGRAMDATA%` |
| `XDG_DATA_HOME` | `~/.local/share` | `~/Library/Application Support` | `%APPDATA%` |
| `XDG_CONFIG_DIRS` | [`/etc/xdg`] | [`/Library/Application Support`] | `%PROGRAMDATA%` |
| `XDG_CONFIG_HOME` | `~/.config` | `~/Library/Application Support` | `%APPDATA%` |
| `XDG_CACHE_HOME` | `~/.cache` | `~/Library/Cache` | `%LOCALAPPDATA%` |
## Notes
- This package does not merge files if they exist across different directories.
- The `Query` methods search through the system variables, `DIRS`, first (when using environment variables first in the variable has presidence). It then checks home variables, `HOME`.
- This package will not create any directories for you. In the standard, it states the following:
> If, when attempting to write a file, the destination directory is non-existant an attempt should be made to create it with permission `0700`. If the destination directory exists already the permissions should not be changed. The application should be prepared to handle the case where the file could not be written, either because the directory was non-existant and could not be created, or for any other reason. In such case it may chose to present an error message to the user.

View File

@ -1,16 +0,0 @@
version: 0.0.1_{build}
build: off
platform: x64
clone_folder: c:\gopath\src\github.com\OpenPeeDeeP\xdg
environment:
GOPATH: c:\gopath
stack: go 1.11
install:
- go get -t -v ./...
- cinst codecov
before_test:
- go vet ./...
test_script:
- go test -v -race -covermode=atomic -coverprofile=coverage.txt
on_success:
- codecov -f coverage.txt

View File

@ -1,8 +0,0 @@
module github.com/OpenPeeDeeP/xdg
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.2.2
)

View File

@ -1,8 +0,0 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=

View File

@ -1,163 +0,0 @@
// Copyright (c) 2017, OpenPeeDeeP. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package xdg impelements the XDG standard for application file locations.
package xdg
import (
"os"
"path/filepath"
"strings"
)
var defaulter xdgDefaulter = new(osDefaulter)
type xdgDefaulter interface {
defaultDataHome() string
defaultDataDirs() []string
defaultConfigHome() string
defaultConfigDirs() []string
defaultCacheHome() string
}
type osDefaulter struct {
}
//This method is used in the testing suit
// nolint: deadcode
func setDefaulter(def xdgDefaulter) {
defaulter = def
}
// XDG is information about the currently running application
type XDG struct {
Vendor string
Application string
}
// New returns an instance of XDG that is used to grab files for application use
func New(vendor, application string) *XDG {
return &XDG{
Vendor: vendor,
Application: application,
}
}
// DataHome returns the location that should be used for user specific data files for this specific application
func (x *XDG) DataHome() string {
return filepath.Join(DataHome(), x.Vendor, x.Application)
}
// DataDirs returns a list of locations that should be used for system wide data files for this specific application
func (x *XDG) DataDirs() []string {
dataDirs := DataDirs()
for i, dir := range dataDirs {
dataDirs[i] = filepath.Join(dir, x.Vendor, x.Application)
}
return dataDirs
}
// ConfigHome returns the location that should be used for user specific config files for this specific application
func (x *XDG) ConfigHome() string {
return filepath.Join(ConfigHome(), x.Vendor, x.Application)
}
// ConfigDirs returns a list of locations that should be used for system wide config files for this specific application
func (x *XDG) ConfigDirs() []string {
configDirs := ConfigDirs()
for i, dir := range configDirs {
configDirs[i] = filepath.Join(dir, x.Vendor, x.Application)
}
return configDirs
}
// CacheHome returns the location that should be used for application cache files for this specific application
func (x *XDG) CacheHome() string {
return filepath.Join(CacheHome(), x.Vendor, x.Application)
}
// QueryData looks for the given filename in XDG paths for data files.
// Returns an empty string if one was not found.
func (x *XDG) QueryData(filename string) string {
dirs := x.DataDirs()
dirs = append([]string{x.DataHome()}, dirs...)
return returnExist(filename, dirs)
}
// QueryConfig looks for the given filename in XDG paths for config files.
// Returns an empty string if one was not found.
func (x *XDG) QueryConfig(filename string) string {
dirs := x.ConfigDirs()
dirs = append([]string{x.ConfigHome()}, dirs...)
return returnExist(filename, dirs)
}
// QueryCache looks for the given filename in XDG paths for cache files.
// Returns an empty string if one was not found.
func (x *XDG) QueryCache(filename string) string {
return returnExist(filename, []string{x.CacheHome()})
}
func returnExist(filename string, dirs []string) string {
for _, dir := range dirs {
_, err := os.Stat(filepath.Join(dir, filename))
if (err != nil && os.IsExist(err)) || err == nil {
return filepath.Join(dir, filename)
}
}
return ""
}
// DataHome returns the location that should be used for user specific data files
func DataHome() string {
dataHome := os.Getenv("XDG_DATA_HOME")
if dataHome == "" {
dataHome = defaulter.defaultDataHome()
}
return dataHome
}
// DataDirs returns a list of locations that should be used for system wide data files
func DataDirs() []string {
var dataDirs []string
dataDirsStr := os.Getenv("XDG_DATA_DIRS")
if dataDirsStr != "" {
dataDirs = strings.Split(dataDirsStr, string(os.PathListSeparator))
}
if len(dataDirs) == 0 {
dataDirs = defaulter.defaultDataDirs()
}
return dataDirs
}
// ConfigHome returns the location that should be used for user specific config files
func ConfigHome() string {
configHome := os.Getenv("XDG_CONFIG_HOME")
if configHome == "" {
configHome = defaulter.defaultConfigHome()
}
return configHome
}
// ConfigDirs returns a list of locations that should be used for system wide config files
func ConfigDirs() []string {
var configDirs []string
configDirsStr := os.Getenv("XDG_CONFIG_DIRS")
if configDirsStr != "" {
configDirs = strings.Split(configDirsStr, string(os.PathListSeparator))
}
if len(configDirs) == 0 {
configDirs = defaulter.defaultConfigDirs()
}
return configDirs
}
// CacheHome returns the location that should be used for application cache files
func CacheHome() string {
cacheHome := os.Getenv("XDG_CACHE_HOME")
if cacheHome == "" {
cacheHome = defaulter.defaultCacheHome()
}
return cacheHome
}

View File

@ -1,30 +0,0 @@
// Copyright (c) 2017, OpenPeeDeeP. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package xdg
import (
"os"
"path/filepath"
)
func (o *osDefaulter) defaultDataHome() string {
return filepath.Join(os.Getenv("HOME"), "Library", "Application Support")
}
func (o *osDefaulter) defaultDataDirs() []string {
return []string{filepath.Join("/Library", "Application Support")}
}
func (o *osDefaulter) defaultConfigHome() string {
return filepath.Join(os.Getenv("HOME"), "Library", "Application Support")
}
func (o *osDefaulter) defaultConfigDirs() []string {
return []string{filepath.Join("/Library", "Application Support")}
}
func (o *osDefaulter) defaultCacheHome() string {
return filepath.Join(os.Getenv("HOME"), "Library", "Caches")
}

View File

@ -1,30 +0,0 @@
// Copyright (c) 2017, OpenPeeDeeP. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package xdg
import (
"os"
"path/filepath"
)
func (o *osDefaulter) defaultDataHome() string {
return filepath.Join(os.Getenv("HOME"), ".local", "share")
}
func (o *osDefaulter) defaultDataDirs() []string {
return []string{"/usr/local/share/", "/usr/share/"}
}
func (o *osDefaulter) defaultConfigHome() string {
return filepath.Join(os.Getenv("HOME"), ".config")
}
func (o *osDefaulter) defaultConfigDirs() []string {
return []string{"/etc/xdg"}
}
func (o *osDefaulter) defaultCacheHome() string {
return filepath.Join(os.Getenv("HOME"), ".cache")
}

View File

@ -1,27 +0,0 @@
// Copyright (c) 2017, OpenPeeDeeP. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package xdg
import "os"
func (o *osDefaulter) defaultDataHome() string {
return os.Getenv("APPDATA")
}
func (o *osDefaulter) defaultDataDirs() []string {
return []string{os.Getenv("PROGRAMDATA")}
}
func (o *osDefaulter) defaultConfigHome() string {
return os.Getenv("APPDATA")
}
func (o *osDefaulter) defaultConfigDirs() []string {
return []string{os.Getenv("PROGRAMDATA")}
}
func (o *osDefaulter) defaultCacheHome() string {
return os.Getenv("LOCALAPPDATA")
}

21
vendor/github.com/gorilla/websocket/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,21 @@
language: go
sudo: false
matrix:
include:
- go: 1.4
- go: 1.5.x
- go: 1.6.x
- go: 1.7.x
- go: 1.8.x
- go: 1.9.x
- go: 1.10.x
- go: tip
allow_failures:
- go: tip
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d .)
- go vet $(go list ./... | grep -v /vendor/)
- go test -v -race ./...

View File

@ -1,14 +1,14 @@
# Gorilla WebSocket
[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket)
[![CircleCI](https://circleci.com/gh/gorilla/websocket.svg?style=svg)](https://circleci.com/gh/gorilla/websocket)
Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
[![Build Status](https://travis-ci.org/gorilla/websocket.svg?branch=master)](https://travis-ci.org/gorilla/websocket)
[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket)
### Documentation
* [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc)
* [API Reference](http://godoc.org/github.com/gorilla/websocket)
* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat)
* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command)
* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo)
@ -27,7 +27,7 @@ package API is stable.
### Protocol Compliance
The Gorilla WebSocket package passes the server tests in the [Autobahn Test
Suite](https://github.com/crossbario/autobahn-testsuite) using the application in the [examples/autobahn
Suite](http://autobahn.ws/testsuite) using the application in the [examples/autobahn
subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn).
### Gorilla WebSocket compared with other packages
@ -40,7 +40,7 @@ subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn
</tr>
<tr>
<tr><td colspan="3"><a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a> Features</td></tr>
<tr><td>Passes <a href="https://github.com/crossbario/autobahn-testsuite">Autobahn Test Suite</a></td><td><a href="https://github.com/gorilla/websocket/tree/master/examples/autobahn">Yes</a></td><td>No</td></tr>
<tr><td>Passes <a href="http://autobahn.ws/testsuite/">Autobahn Test Suite</a></td><td><a href="https://github.com/gorilla/websocket/tree/master/examples/autobahn">Yes</a></td><td>No</td></tr>
<tr><td>Receive <a href="https://tools.ietf.org/html/rfc6455#section-5.4">fragmented</a> message<td>Yes</td><td><a href="https://code.google.com/p/go/issues/detail?id=7632">No</a>, see note 1</td></tr>
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.1">close</a> message</td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td><a href="https://code.google.com/p/go/issues/detail?id=4588">No</a></td></tr>
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.2">pings</a> and receive <a href="https://tools.ietf.org/html/rfc6455#section-5.5.3">pongs</a></td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td>No</td></tr>

View File

@ -6,14 +6,12 @@ package websocket
import (
"bytes"
"context"
"crypto/tls"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptrace"
"net/url"
"strings"
"time"
@ -53,10 +51,6 @@ type Dialer struct {
// NetDial is nil, net.Dial is used.
NetDial func(network, addr string) (net.Conn, error)
// NetDialContext specifies the dial function for creating TCP connections. If
// NetDialContext is nil, net.DialContext is used.
NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error)
// Proxy specifies a function to return a proxy for a given
// Request. If the function returns a non-nil error, the
// request is aborted with the provided error.
@ -70,22 +64,11 @@ type Dialer struct {
// HandshakeTimeout specifies the duration for the handshake to complete.
HandshakeTimeout time.Duration
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer
// size is zero, then a useful default size is used. The I/O buffer sizes
// do not limit the size of the messages that can be sent or received.
ReadBufferSize, WriteBufferSize int
// WriteBufferPool is a pool of buffers for write operations. If the value
// is not set, then write buffers are allocated to the connection for the
// lifetime of the connection.
//
// A pool is most useful when the application has a modest volume of writes
// across a large number of connections.
//
// Applications should use a single pool for each unique value of
// WriteBufferSize.
WriteBufferPool BufferPool
// Subprotocols specifies the client's requested subprotocols.
Subprotocols []string
@ -101,11 +84,6 @@ type Dialer struct {
Jar http.CookieJar
}
// Dial creates a new client connection by calling DialContext with a background context.
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
return d.DialContext(context.Background(), urlStr, requestHeader)
}
var errMalformedURL = errors.New("malformed ws or wss URL")
func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
@ -133,20 +111,19 @@ var DefaultDialer = &Dialer{
}
// nilDialer is dialer to use when receiver is nil.
var nilDialer = *DefaultDialer
var nilDialer Dialer = *DefaultDialer
// DialContext creates a new client connection. Use requestHeader to specify the
// Dial creates a new client connection. Use requestHeader to specify the
// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
// Use the response.Header to get the selected subprotocol
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
//
// The context will be used in the request and in the Dialer.
//
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
// non-nil *http.Response so that callers can handle redirects, authentication,
// etcetera. The response body may not contain the entire response and does not
// need to be closed by the application.
func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
if d == nil {
d = &nilDialer
}
@ -184,7 +161,6 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h
Header: make(http.Header),
Host: u.Host,
}
req = req.WithContext(ctx)
// Set the cookies present in the cookie jar of the dialer
if d.Jar != nil {
@ -228,30 +204,20 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h
req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"}
}
var deadline time.Time
if d.HandshakeTimeout != 0 {
var cancel func()
ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout)
defer cancel()
deadline = time.Now().Add(d.HandshakeTimeout)
}
// Get network dial function.
var netDial func(network, add string) (net.Conn, error)
if d.NetDialContext != nil {
netDial = func(network, addr string) (net.Conn, error) {
return d.NetDialContext(ctx, network, addr)
}
} else if d.NetDial != nil {
netDial = d.NetDial
} else {
netDialer := &net.Dialer{}
netDial = func(network, addr string) (net.Conn, error) {
return netDialer.DialContext(ctx, network, addr)
}
netDial := d.NetDial
if netDial == nil {
netDialer := &net.Dialer{Deadline: deadline}
netDial = netDialer.Dial
}
// If needed, wrap the dial function to set the connection deadline.
if deadline, ok := ctx.Deadline(); ok {
if !deadline.Equal(time.Time{}) {
forwardDial := netDial
netDial = func(network, addr string) (net.Conn, error) {
c, err := forwardDial(network, addr)
@ -283,17 +249,7 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h
}
hostPort, hostNoPort := hostPortNoPort(u)
trace := httptrace.ContextClientTrace(ctx)
if trace != nil && trace.GetConn != nil {
trace.GetConn(hostPort)
}
netConn, err := netDial("tcp", hostPort)
if trace != nil && trace.GotConn != nil {
trace.GotConn(httptrace.GotConnInfo{
Conn: netConn,
})
}
if err != nil {
return nil, nil, err
}
@ -311,31 +267,22 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h
}
tlsConn := tls.Client(netConn, cfg)
netConn = tlsConn
var err error
if trace != nil {
err = doHandshakeWithTrace(trace, tlsConn, cfg)
} else {
err = doHandshake(tlsConn, cfg)
}
if err != nil {
if err := tlsConn.Handshake(); err != nil {
return nil, nil, err
}
if !cfg.InsecureSkipVerify {
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
return nil, nil, err
}
}
}
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil)
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize)
if err := req.Write(netConn); err != nil {
return nil, nil, err
}
if trace != nil && trace.GotFirstResponseByte != nil {
if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 {
trace.GotFirstResponseByte()
}
}
resp, err := http.ReadResponse(conn.br, req)
if err != nil {
return nil, nil, err
@ -381,15 +328,3 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h
netConn = nil // to avoid close in defer.
return conn, resp, nil
}
func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error {
if err := tlsConn.Handshake(); err != nil {
return err
}
if !cfg.InsecureSkipVerify {
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
return err
}
}
return nil
}

View File

@ -223,20 +223,6 @@ func isValidReceivedCloseCode(code int) bool {
return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999)
}
// BufferPool represents a pool of buffers. The *sync.Pool type satisfies this
// interface. The type of the value stored in a pool is not specified.
type BufferPool interface {
// Get gets a value from the pool or returns nil if the pool is empty.
Get() interface{}
// Put adds a value to the pool.
Put(interface{})
}
// writePoolData is the type added to the write buffer pool. This wrapper is
// used to prevent applications from peeking at and depending on the values
// added to the pool.
type writePoolData struct{ buf []byte }
// The Conn type represents a WebSocket connection.
type Conn struct {
conn net.Conn
@ -244,10 +230,8 @@ type Conn struct {
subprotocol string
// Write fields
mu chan struct{} // used as mutex to protect write to conn
writeBuf []byte // frame is constructed in this buffer.
writePool BufferPool
writeBufSize int
mu chan bool // used as mutex to protect write to conn
writeBuf []byte // frame is constructed in this buffer.
writeDeadline time.Time
writer io.WriteCloser // the current writer returned to the application
isWriting bool // for best-effort concurrent write detection
@ -260,12 +244,10 @@ type Conn struct {
newCompressionWriter func(io.WriteCloser, int) io.WriteCloser
// Read fields
reader io.ReadCloser // the current reader returned to the application
readErr error
br *bufio.Reader
// bytes remaining in current frame.
// set setReadRemaining to safely update this value and prevent overflow
readRemaining int64
reader io.ReadCloser // the current reader returned to the application
readErr error
br *bufio.Reader
readRemaining int64 // bytes remaining in current frame.
readFinal bool // true the current message has more frames.
readLength int64 // Message size.
readLimit int64 // Maximum message size.
@ -281,29 +263,64 @@ type Conn struct {
newDecompressionReader func(io.Reader) io.ReadCloser
}
func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn {
func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn {
return newConnBRW(conn, isServer, readBufferSize, writeBufferSize, nil)
}
type writeHook struct {
p []byte
}
func (wh *writeHook) Write(p []byte) (int, error) {
wh.p = p
return len(p), nil
}
func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, brw *bufio.ReadWriter) *Conn {
mu := make(chan bool, 1)
mu <- true
var br *bufio.Reader
if readBufferSize == 0 && brw != nil && brw.Reader != nil {
// Reuse the supplied bufio.Reader if the buffer has a useful size.
// This code assumes that peek on a reader returns
// bufio.Reader.buf[:0].
brw.Reader.Reset(conn)
if p, err := brw.Reader.Peek(0); err == nil && cap(p) >= 256 {
br = brw.Reader
}
}
if br == nil {
if readBufferSize == 0 {
readBufferSize = defaultReadBufferSize
} else if readBufferSize < maxControlFramePayloadSize {
// must be large enough for control frame
}
if readBufferSize < maxControlFramePayloadSize {
readBufferSize = maxControlFramePayloadSize
}
br = bufio.NewReaderSize(conn, readBufferSize)
}
if writeBufferSize <= 0 {
writeBufferSize = defaultWriteBufferSize
}
writeBufferSize += maxFrameHeaderSize
if writeBuf == nil && writeBufferPool == nil {
writeBuf = make([]byte, writeBufferSize)
var writeBuf []byte
if writeBufferSize == 0 && brw != nil && brw.Writer != nil {
// Use the bufio.Writer's buffer if the buffer has a useful size. This
// code assumes that bufio.Writer.buf[:1] is passed to the
// bufio.Writer's underlying writer.
var wh writeHook
brw.Writer.Reset(&wh)
brw.Writer.WriteByte(0)
brw.Flush()
if cap(wh.p) >= maxFrameHeaderSize+256 {
writeBuf = wh.p[:cap(wh.p)]
}
}
if writeBuf == nil {
if writeBufferSize == 0 {
writeBufferSize = defaultWriteBufferSize
}
writeBuf = make([]byte, writeBufferSize+maxFrameHeaderSize)
}
mu := make(chan struct{}, 1)
mu <- struct{}{}
c := &Conn{
isServer: isServer,
br: br,
@ -311,8 +328,6 @@ func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int,
mu: mu,
readFinal: true,
writeBuf: writeBuf,
writePool: writeBufferPool,
writeBufSize: writeBufferSize,
enableWriteCompression: true,
compressionLevel: defaultCompressionLevel,
}
@ -322,17 +337,6 @@ func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int,
return c
}
// setReadRemaining tracks the number of bytes remaining on the connection. If n
// overflows, an ErrReadLimit is returned.
func (c *Conn) setReadRemaining(n int64) error {
if n < 0 {
return ErrReadLimit
}
c.readRemaining = n
return nil
}
// Subprotocol returns the negotiated protocol for the connection.
func (c *Conn) Subprotocol() string {
return c.subprotocol
@ -366,18 +370,9 @@ func (c *Conn) writeFatal(err error) error {
return err
}
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
c.br.Discard(len(p))
return p, err
}
func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error {
<-c.mu
defer func() { c.mu <- struct{}{} }()
defer func() { c.mu <- true }()
c.writeErrMu.Lock()
err := c.writeErr
@ -429,7 +424,7 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er
maskBytes(key, 0, buf[6:])
}
d := 1000 * time.Hour
d := time.Hour * 1000
if !deadline.IsZero() {
d = deadline.Sub(time.Now())
if d < 0 {
@ -444,7 +439,7 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er
case <-timer.C:
return errWriteTimeout
}
defer func() { c.mu <- struct{}{} }()
defer func() { c.mu <- true }()
c.writeErrMu.Lock()
err := c.writeErr
@ -464,8 +459,7 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er
return err
}
// beginMessage prepares a connection and message writer for a new message.
func (c *Conn) beginMessage(mw *messageWriter, messageType int) error {
func (c *Conn) prepWrite(messageType int) error {
// Close previous writer if not already closed by the application. It's
// probably better to return an error in this situation, but we cannot
// change this without breaking existing applications.
@ -481,23 +475,7 @@ func (c *Conn) beginMessage(mw *messageWriter, messageType int) error {
c.writeErrMu.Lock()
err := c.writeErr
c.writeErrMu.Unlock()
if err != nil {
return err
}
mw.c = c
mw.frameType = messageType
mw.pos = maxFrameHeaderSize
if c.writeBuf == nil {
wpd, ok := c.writePool.Get().(writePoolData)
if ok {
c.writeBuf = wpd.buf
} else {
c.writeBuf = make([]byte, c.writeBufSize)
}
}
return nil
return err
}
// NextWriter returns a writer for the next message to send. The writer's Close
@ -509,11 +487,16 @@ func (c *Conn) beginMessage(mw *messageWriter, messageType int) error {
// All message types (TextMessage, BinaryMessage, CloseMessage, PingMessage and
// PongMessage) are supported.
func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {
var mw messageWriter
if err := c.beginMessage(&mw, messageType); err != nil {
if err := c.prepWrite(messageType); err != nil {
return nil, err
}
c.writer = &mw
mw := &messageWriter{
c: c,
frameType: messageType,
pos: maxFrameHeaderSize,
}
c.writer = mw
if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) {
w := c.newCompressionWriter(c.writer, c.compressionLevel)
mw.compress = true
@ -530,16 +513,10 @@ type messageWriter struct {
err error
}
func (w *messageWriter) endMessage(err error) error {
func (w *messageWriter) fatal(err error) error {
if w.err != nil {
return err
}
c := w.c
w.err = err
c.writer = nil
if c.writePool != nil {
c.writePool.Put(writePoolData{buf: c.writeBuf})
c.writeBuf = nil
w.err = err
w.c.writer = nil
}
return err
}
@ -553,7 +530,7 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error {
// Check for invalid control frames.
if isControl(w.frameType) &&
(!final || length > maxControlFramePayloadSize) {
return w.endMessage(errInvalidControlFrame)
return w.fatal(errInvalidControlFrame)
}
b0 := byte(w.frameType)
@ -598,7 +575,7 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error {
copy(c.writeBuf[maxFrameHeaderSize-4:], key[:])
maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:w.pos])
if len(extra) > 0 {
return w.endMessage(c.writeFatal(errors.New("websocket: internal error, extra used in client mode")))
return c.writeFatal(errors.New("websocket: internal error, extra used in client mode"))
}
}
@ -619,11 +596,11 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error {
c.isWriting = false
if err != nil {
return w.endMessage(err)
return w.fatal(err)
}
if final {
w.endMessage(errWriteClosed)
c.writer = nil
return nil
}
@ -721,7 +698,11 @@ func (w *messageWriter) Close() error {
if w.err != nil {
return w.err
}
return w.flushFrame(true, nil)
if err := w.flushFrame(true, nil); err != nil {
return err
}
w.err = errWriteClosed
return nil
}
// WritePreparedMessage writes prepared message into connection.
@ -753,10 +734,10 @@ func (c *Conn) WriteMessage(messageType int, data []byte) error {
if c.isServer && (c.newCompressionWriter == nil || !c.enableWriteCompression) {
// Fast path with no allocations and single frame.
var mw messageWriter
if err := c.beginMessage(&mw, messageType); err != nil {
if err := c.prepWrite(messageType); err != nil {
return err
}
mw := messageWriter{c: c, frameType: messageType, pos: maxFrameHeaderSize}
n := copy(c.writeBuf[mw.pos:], data)
mw.pos += n
data = data[n:]
@ -803,7 +784,7 @@ func (c *Conn) advanceFrame() (int, error) {
final := p[0]&finalBit != 0
frameType := int(p[0] & 0xf)
mask := p[1]&maskBit != 0
c.setReadRemaining(int64(p[1] & 0x7f))
c.readRemaining = int64(p[1] & 0x7f)
c.readDecompress = false
if c.newDecompressionReader != nil && (p[0]&rsv1Bit) != 0 {
@ -837,17 +818,7 @@ func (c *Conn) advanceFrame() (int, error) {
return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType))
}
// 3. Read and parse frame length as per
// https://tools.ietf.org/html/rfc6455#section-5.2
//
// The length of the "Payload data", in bytes: if 0-125, that is the payload
// length.
// - If 126, the following 2 bytes interpreted as a 16-bit unsigned
// integer are the payload length.
// - If 127, the following 8 bytes interpreted as
// a 64-bit unsigned integer (the most significant bit MUST be 0) are the
// payload length. Multibyte length quantities are expressed in network byte
// order.
// 3. Read and parse frame length.
switch c.readRemaining {
case 126:
@ -855,19 +826,13 @@ func (c *Conn) advanceFrame() (int, error) {
if err != nil {
return noFrame, err
}
if err := c.setReadRemaining(int64(binary.BigEndian.Uint16(p))); err != nil {
return noFrame, err
}
c.readRemaining = int64(binary.BigEndian.Uint16(p))
case 127:
p, err := c.read(8)
if err != nil {
return noFrame, err
}
if err := c.setReadRemaining(int64(binary.BigEndian.Uint64(p))); err != nil {
return noFrame, err
}
c.readRemaining = int64(binary.BigEndian.Uint64(p))
}
// 4. Handle frame masking.
@ -890,12 +855,6 @@ func (c *Conn) advanceFrame() (int, error) {
if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage {
c.readLength += c.readRemaining
// Don't allow readLength to overflow in the presence of a large readRemaining
// counter.
if c.readLength < 0 {
return noFrame, ErrReadLimit
}
if c.readLimit > 0 && c.readLength > c.readLimit {
c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait))
return noFrame, ErrReadLimit
@ -909,7 +868,7 @@ func (c *Conn) advanceFrame() (int, error) {
var payload []byte
if c.readRemaining > 0 {
payload, err = c.read(int(c.readRemaining))
c.setReadRemaining(0)
c.readRemaining = 0
if err != nil {
return noFrame, err
}
@ -982,7 +941,6 @@ func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
c.readErr = hideTempErr(err)
break
}
if frameType == TextMessage || frameType == BinaryMessage {
c.messageReader = &messageReader{c}
c.reader = c.messageReader
@ -1023,9 +981,7 @@ func (r *messageReader) Read(b []byte) (int, error) {
if c.isServer {
c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n])
}
rem := c.readRemaining
rem -= int64(n)
c.setReadRemaining(rem)
c.readRemaining -= int64(n)
if c.readRemaining > 0 && c.readErr == io.EOF {
c.readErr = errUnexpectedEOF
}
@ -1077,7 +1033,7 @@ func (c *Conn) SetReadDeadline(t time.Time) error {
return c.conn.SetReadDeadline(t)
}
// SetReadLimit sets the maximum size in bytes for a message read from the peer. If a
// SetReadLimit sets the maximum size for a message read from the peer. If a
// message exceeds the limit, the connection sends a close message to the peer
// and returns ErrReadLimit to the application.
func (c *Conn) SetReadLimit(limit int64) {

18
vendor/github.com/gorilla/websocket/conn_read.go generated vendored Normal file
View File

@ -0,0 +1,18 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.5
package websocket
import "io"
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
c.br.Discard(len(p))
return p, err
}

View File

@ -0,0 +1,21 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.5
package websocket
import "io"
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
if len(p) > 0 {
// advance over the bytes just read
io.ReadFull(c.br, p)
}
return p, err
}

View File

@ -151,53 +151,6 @@
// checking. The application is responsible for checking the Origin header
// before calling the Upgrade function.
//
// Buffers
//
// Connections buffer network input and output to reduce the number
// of system calls when reading or writing messages.
//
// Write buffers are also used for constructing WebSocket frames. See RFC 6455,
// Section 5 for a discussion of message framing. A WebSocket frame header is
// written to the network each time a write buffer is flushed to the network.
// Decreasing the size of the write buffer can increase the amount of framing
// overhead on the connection.
//
// The buffer sizes in bytes are specified by the ReadBufferSize and
// WriteBufferSize fields in the Dialer and Upgrader. The Dialer uses a default
// size of 4096 when a buffer size field is set to zero. The Upgrader reuses
// buffers created by the HTTP server when a buffer size field is set to zero.
// The HTTP server buffers have a size of 4096 at the time of this writing.
//
// The buffer sizes do not limit the size of a message that can be read or
// written by a connection.
//
// Buffers are held for the lifetime of the connection by default. If the
// Dialer or Upgrader WriteBufferPool field is set, then a connection holds the
// write buffer only when writing a message.
//
// Applications should tune the buffer sizes to balance memory use and
// performance. Increasing the buffer size uses more memory, but can reduce the
// number of system calls to read or write the network. In the case of writing,
// increasing the buffer size can reduce the number of frame headers written to
// the network.
//
// Some guidelines for setting buffer parameters are:
//
// Limit the buffer sizes to the maximum expected message size. Buffers larger
// than the largest message do not provide any benefit.
//
// Depending on the distribution of message sizes, setting the buffer size to
// a value less than the maximum expected message size can greatly reduce memory
// use with a small impact on performance. Here's an example: If 99% of the
// messages are smaller than 256 bytes and the maximum message size is 512
// bytes, then a buffer size of 256 bytes will result in 1.01 more system calls
// than a buffer size of 512 bytes. The memory savings is 50%.
//
// A write buffer pool is useful when the application has a modest number
// writes over a large number of connections. when buffers are pooled, a larger
// buffer size has a reduced impact on total memory use and has the benefit of
// reducing system calls and frame overhead.
//
// Compression EXPERIMENTAL
//
// Per message compression extensions (RFC 7692) are experimentally supported

View File

@ -1,3 +0,0 @@
module github.com/gorilla/websocket
go 1.12

View File

View File

@ -1,42 +0,0 @@
// Copyright 2019 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"io"
"strings"
)
// JoinMessages concatenates received messages to create a single io.Reader.
// The string term is appended to each message. The returned reader does not
// support concurrent calls to the Read method.
func JoinMessages(c *Conn, term string) io.Reader {
return &joinReader{c: c, term: term}
}
type joinReader struct {
c *Conn
term string
r io.Reader
}
func (r *joinReader) Read(p []byte) (int, error) {
if r.r == nil {
var err error
_, r.r, err = r.c.NextReader()
if err != nil {
return 0, err
}
if r.term != "" {
r.r = io.MultiReader(r.r, strings.NewReader(r.term))
}
}
n, err := r.r.Read(p)
if err == io.EOF {
err = nil
r.r = nil
}
return n, err
}

View File

@ -19,6 +19,7 @@ import (
type PreparedMessage struct {
messageType int
data []byte
err error
mu sync.Mutex
frames map[prepareKey]*preparedFrame
}
@ -73,8 +74,8 @@ func (pm *PreparedMessage) frame(key prepareKey) (int, []byte, error) {
// Prepare a frame using a 'fake' connection.
// TODO: Refactor code in conn.go to allow more direct construction of
// the frame.
mu := make(chan struct{}, 1)
mu <- struct{}{}
mu := make(chan bool, 1)
mu <- true
var nc prepareConn
c := &Conn{
conn: &nc,

View File

@ -22,18 +22,18 @@ func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) {
func init() {
proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) {
return &httpProxyDialer{proxyURL: proxyURL, forwardDial: forwardDialer.Dial}, nil
return &httpProxyDialer{proxyURL: proxyURL, fowardDial: forwardDialer.Dial}, nil
})
}
type httpProxyDialer struct {
proxyURL *url.URL
forwardDial func(network, addr string) (net.Conn, error)
proxyURL *url.URL
fowardDial func(network, addr string) (net.Conn, error)
}
func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) {
hostPort, _ := hostPortNoPort(hpd.proxyURL)
conn, err := hpd.forwardDial(network, hostPort)
conn, err := hpd.fowardDial(network, hostPort)
if err != nil {
return nil, err
}

View File

@ -7,7 +7,7 @@ package websocket
import (
"bufio"
"errors"
"io"
"net"
"net/http"
"net/url"
"strings"
@ -27,23 +27,12 @@ type Upgrader struct {
// HandshakeTimeout specifies the duration for the handshake to complete.
HandshakeTimeout time.Duration
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer
// size is zero, then buffers allocated by the HTTP server are used. The
// I/O buffer sizes do not limit the size of the messages that can be sent
// or received.
ReadBufferSize, WriteBufferSize int
// WriteBufferPool is a pool of buffers for write operations. If the value
// is not set, then write buffers are allocated to the connection for the
// lifetime of the connection.
//
// A pool is most useful when the application has a modest volume of writes
// across a large number of connections.
//
// Applications should use a single pool for each unique value of
// WriteBufferSize.
WriteBufferPool BufferPool
// Subprotocols specifies the server's supported protocols in order of
// preference. If this field is not nil, then the Upgrade method negotiates a
// subprotocol by selecting the first match in this list with a protocol
@ -153,7 +142,7 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
challengeKey := r.Header.Get("Sec-Websocket-Key")
if challengeKey == "" {
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header is missing or blank")
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-WebSocket-Key' header is missing or blank")
}
subprotocol := u.selectSubprotocol(r, responseHeader)
@ -170,12 +159,17 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
}
}
var (
netConn net.Conn
err error
)
h, ok := w.(http.Hijacker)
if !ok {
return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")
}
var brw *bufio.ReadWriter
netConn, brw, err := h.Hijack()
netConn, brw, err = h.Hijack()
if err != nil {
return u.returnError(w, r, http.StatusInternalServerError, err.Error())
}
@ -185,21 +179,7 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
return nil, errors.New("websocket: client sent data before handshake is complete")
}
var br *bufio.Reader
if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 {
// Reuse hijacked buffered reader as connection reader.
br = brw.Reader
}
buf := bufioWriterBuffer(netConn, brw.Writer)
var writeBuf []byte
if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 {
// Reuse hijacked write buffer as connection buffer.
writeBuf = buf
}
c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf)
c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw)
c.subprotocol = subprotocol
if compress {
@ -207,13 +187,7 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
c.newDecompressionReader = decompressNoContextTakeover
}
// Use larger of hijacked buffer and connection write buffer for header.
p := buf
if len(c.writeBuf) > len(p) {
p = c.writeBuf
}
p = p[:0]
p := c.writeBuf[:0]
p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
p = append(p, computeAcceptKey(challengeKey)...)
p = append(p, "\r\n"...)
@ -324,40 +298,3 @@ func IsWebSocketUpgrade(r *http.Request) bool {
return tokenListContainsValue(r.Header, "Connection", "upgrade") &&
tokenListContainsValue(r.Header, "Upgrade", "websocket")
}
// bufioReaderSize size returns the size of a bufio.Reader.
func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int {
// This code assumes that peek on a reset reader returns
// bufio.Reader.buf[:0].
// TODO: Use bufio.Reader.Size() after Go 1.10
br.Reset(originalReader)
if p, err := br.Peek(0); err == nil {
return cap(p)
}
return 0
}
// writeHook is an io.Writer that records the last slice passed to it vio
// io.Writer.Write.
type writeHook struct {
p []byte
}
func (wh *writeHook) Write(p []byte) (int, error) {
wh.p = p
return len(p), nil
}
// bufioWriterBuffer grabs the buffer from a bufio.Writer.
func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte {
// This code assumes that bufio.Writer.buf[:1] is passed to the
// bufio.Writer's underlying writer.
var wh writeHook
bw.Reset(&wh)
bw.WriteByte(0)
bw.Flush()
bw.Reset(originalWriter)
return wh.p[:cap(wh.p)]
}

View File

@ -1,19 +0,0 @@
// +build go1.8
package websocket
import (
"crypto/tls"
"net/http/httptrace"
)
func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error {
if trace.TLSHandshakeStart != nil {
trace.TLSHandshakeStart()
}
err := doHandshake(tlsConn, cfg)
if trace.TLSHandshakeDone != nil {
trace.TLSHandshakeDone(tlsConn.ConnectionState(), err)
}
return err
}

View File

@ -1,12 +0,0 @@
// +build !go1.8
package websocket
import (
"crypto/tls"
"net/http/httptrace"
)
func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error {
return doHandshake(tlsConn, cfg)
}

View File

@ -31,113 +31,68 @@ func generateChallengeKey() (string, error) {
return base64.StdEncoding.EncodeToString(p), nil
}
// Token octets per RFC 2616.
var isTokenOctet = [256]bool{
'!': true,
'#': true,
'$': true,
'%': true,
'&': true,
'\'': true,
'*': true,
'+': true,
'-': true,
'.': true,
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
'A': true,
'B': true,
'C': true,
'D': true,
'E': true,
'F': true,
'G': true,
'H': true,
'I': true,
'J': true,
'K': true,
'L': true,
'M': true,
'N': true,
'O': true,
'P': true,
'Q': true,
'R': true,
'S': true,
'T': true,
'U': true,
'W': true,
'V': true,
'X': true,
'Y': true,
'Z': true,
'^': true,
'_': true,
'`': true,
'a': true,
'b': true,
'c': true,
'd': true,
'e': true,
'f': true,
'g': true,
'h': true,
'i': true,
'j': true,
'k': true,
'l': true,
'm': true,
'n': true,
'o': true,
'p': true,
'q': true,
'r': true,
's': true,
't': true,
'u': true,
'v': true,
'w': true,
'x': true,
'y': true,
'z': true,
'|': true,
'~': true,
// Octet types from RFC 2616.
var octetTypes [256]byte
const (
isTokenOctet = 1 << iota
isSpaceOctet
)
func init() {
// From RFC 2616
//
// OCTET = <any 8-bit sequence of data>
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
// CR = <US-ASCII CR, carriage return (13)>
// LF = <US-ASCII LF, linefeed (10)>
// SP = <US-ASCII SP, space (32)>
// HT = <US-ASCII HT, horizontal-tab (9)>
// <"> = <US-ASCII double-quote mark (34)>
// CRLF = CR LF
// LWS = [CRLF] 1*( SP | HT )
// TEXT = <any OCTET except CTLs, but including LWS>
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
// token = 1*<any CHAR except CTLs or separators>
// qdtext = <any TEXT except <">>
for c := 0; c < 256; c++ {
var t byte
isCtl := c <= 31 || c == 127
isChar := 0 <= c && c <= 127
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
t |= isSpaceOctet
}
if isChar && !isCtl && !isSeparator {
t |= isTokenOctet
}
octetTypes[c] = t
}
}
// skipSpace returns a slice of the string s with all leading RFC 2616 linear
// whitespace removed.
func skipSpace(s string) (rest string) {
i := 0
for ; i < len(s); i++ {
if b := s[i]; b != ' ' && b != '\t' {
if octetTypes[s[i]]&isSpaceOctet == 0 {
break
}
}
return s[i:]
}
// nextToken returns the leading RFC 2616 token of s and the string following
// the token.
func nextToken(s string) (token, rest string) {
i := 0
for ; i < len(s); i++ {
if !isTokenOctet[s[i]] {
if octetTypes[s[i]]&isTokenOctet == 0 {
break
}
}
return s[:i], s[i:]
}
// nextTokenOrQuoted returns the leading token or quoted string per RFC 2616
// and the string following the token or quoted string.
func nextTokenOrQuoted(s string) (value string, rest string) {
if !strings.HasPrefix(s, "\"") {
return nextToken(s)
@ -173,8 +128,7 @@ func nextTokenOrQuoted(s string) (value string, rest string) {
return "", ""
}
// equalASCIIFold returns true if s is equal to t with ASCII case folding as
// defined in RFC 4790.
// equalASCIIFold returns true if s is equal to t with ASCII case folding.
func equalASCIIFold(s, t string) bool {
for s != "" && t != "" {
sr, size := utf8.DecodeRuneInString(s)
@ -224,7 +178,7 @@ headers:
return false
}
// parseExtensions parses WebSocket extensions from a header.
// parseExtensiosn parses WebSocket extensions from a header.
func parseExtensions(header http.Header) []map[string]string {
// From RFC 6455:
//

View File

@ -1,253 +0,0 @@
// 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 (
"bytes"
"unicode"
"unicode/utf8"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
)
var foldTransformer = unicodeFoldTransformer{}
var noopTransformer = transform.Nop
var normalizeTransformer = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
var normalizeFoldTransformer = transform.Chain(normalizeTransformer, foldTransformer)
// 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, noopTransformer)
}
// MatchFold is a case-insensitive version of Match.
func MatchFold(source, target string) bool {
return match(source, target, foldTransformer)
}
// MatchNormalized is a unicode-normalized version of Match.
func MatchNormalized(source, target string) bool {
return match(source, target, normalizeTransformer)
}
// MatchNormalizedFold is a unicode-normalized and case-insensitive version of Match.
func MatchNormalizedFold(source, target string) bool {
return match(source, target, normalizeFoldTransformer)
}
func match(source, target string, transformer transform.Transformer) bool {
source = stringTransform(source, transformer)
target = stringTransform(target, transformer)
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 r1 == 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, noopTransformer)
}
// FindFold is a case-insensitive version of Find.
func FindFold(source string, targets []string) []string {
return find(source, targets, foldTransformer)
}
// FindNormalized is a unicode-normalized version of Find.
func FindNormalized(source string, targets []string) []string {
return find(source, targets, normalizeTransformer)
}
// FindNormalizedFold is a unicode-normalized and case-insensitive version of Find.
func FindNormalizedFold(source string, targets []string) []string {
return find(source, targets, normalizeFoldTransformer)
}
func find(source string, targets []string, transformer transform.Transformer) []string {
var matches []string
for _, target := range targets {
if match(source, target, transformer) {
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, noopTransformer)
}
// RankMatchFold is a case-insensitive version of RankMatch.
func RankMatchFold(source, target string) int {
return rank(source, target, foldTransformer)
}
// RankMatchNormalized is a unicode-normalized version of RankMatch.
func RankMatchNormalized(source, target string) int {
return rank(source, target, normalizeTransformer)
}
// RankMatchNormalizedFold is a unicode-normalized and case-insensitive version of RankMatch.
func RankMatchNormalizedFold(source, target string) int {
return rank(source, target, normalizeFoldTransformer)
}
func rank(source, target string, transformer transform.Transformer) int {
lenDiff := len(target) - len(source)
if lenDiff < 0 {
return -1
}
source = stringTransform(source, transformer)
target = stringTransform(target, transformer)
if lenDiff == 0 && source == target {
return 0
}
runeDiff := 0
Outer:
for _, r1 := range source {
for i, r2 := range target {
if r1 == r2 {
target = target[i+utf8.RuneLen(r2):]
continue Outer
} else {
runeDiff++
}
}
return -1
}
// Count up remaining char
runeDiff += utf8.RuneCountInString(target)
return runeDiff
}
// RankFind is similar to Find, except it will also rank all matches using
// Levenshtein distance.
func RankFind(source string, targets []string) Ranks {
return rankFind(source, targets, noopTransformer)
}
// RankFindFold is a case-insensitive version of RankFind.
func RankFindFold(source string, targets []string) Ranks {
return rankFind(source, targets, foldTransformer)
}
// RankFindNormalized is a unicode-normalizedversion of RankFind.
func RankFindNormalized(source string, targets []string) Ranks {
return rankFind(source, targets, normalizeTransformer)
}
// RankFindNormalizedFold is a unicode-normalized and case-insensitive version of RankFind.
func RankFindNormalizedFold(source string, targets []string) Ranks {
return rankFind(source, targets, normalizeFoldTransformer)
}
func rankFind(source string, targets []string, transformer transform.Transformer) Ranks {
var r Ranks
for index, target := range targets {
if match(source, target, transformer) {
distance := LevenshteinDistance(source, target)
r = append(r, Rank{source, target, distance, index})
}
}
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
// Location of Target in original list
OriginalIndex 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
}
func stringTransform(s string, t transform.Transformer) (transformed string) {
var err error
transformed, _, err = transform.String(t, s)
if err != nil {
transformed = s
}
return
}
type unicodeFoldTransformer struct{}
func (unicodeFoldTransformer) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
runes := bytes.Runes(src)
var lowerRunes []rune
for _, r := range runes {
lowerRunes = append(lowerRunes, unicode.ToLower(r))
}
srcBytes := []byte(string(lowerRunes))
n := copy(dst, srcBytes)
if n < len(srcBytes) {
err = transform.ErrShortDst
}
return n, n, err
}
func (unicodeFoldTransformer) Reset() {}

View File

@ -1,3 +0,0 @@
module github.com/mattn/go-runewidth
go 1.9

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
// +build appengine
package runewidth
// IsEastAsian return true if the current locale is CJK
func IsEastAsian() bool {
return false
}

View File

@ -1,5 +1,4 @@
// +build js
// +build !appengine
package runewidth

View File

@ -1,6 +1,4 @@
// +build !windows
// +build !js
// +build !appengine
// +build !windows,!js
package runewidth

View File

@ -1,427 +0,0 @@
package runewidth
var combining = table{
{0x0300, 0x036F}, {0x0483, 0x0489}, {0x07EB, 0x07F3},
{0x0C00, 0x0C00}, {0x0C04, 0x0C04}, {0x0D00, 0x0D01},
{0x135D, 0x135F}, {0x1A7F, 0x1A7F}, {0x1AB0, 0x1ABE},
{0x1B6B, 0x1B73}, {0x1DC0, 0x1DF9}, {0x1DFB, 0x1DFF},
{0x20D0, 0x20F0}, {0x2CEF, 0x2CF1}, {0x2DE0, 0x2DFF},
{0x3099, 0x309A}, {0xA66F, 0xA672}, {0xA674, 0xA67D},
{0xA69E, 0xA69F}, {0xA6F0, 0xA6F1}, {0xA8E0, 0xA8F1},
{0xFE20, 0xFE2F}, {0x101FD, 0x101FD}, {0x10376, 0x1037A},
{0x10F46, 0x10F50}, {0x11300, 0x11301}, {0x1133B, 0x1133C},
{0x11366, 0x1136C}, {0x11370, 0x11374}, {0x16AF0, 0x16AF4},
{0x1D165, 0x1D169}, {0x1D16D, 0x1D172}, {0x1D17B, 0x1D182},
{0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD}, {0x1D242, 0x1D244},
{0x1E000, 0x1E006}, {0x1E008, 0x1E018}, {0x1E01B, 0x1E021},
{0x1E023, 0x1E024}, {0x1E026, 0x1E02A}, {0x1E8D0, 0x1E8D6},
}
var doublewidth = table{
{0x1100, 0x115F}, {0x231A, 0x231B}, {0x2329, 0x232A},
{0x23E9, 0x23EC}, {0x23F0, 0x23F0}, {0x23F3, 0x23F3},
{0x25FD, 0x25FE}, {0x2614, 0x2615}, {0x2648, 0x2653},
{0x267F, 0x267F}, {0x2693, 0x2693}, {0x26A1, 0x26A1},
{0x26AA, 0x26AB}, {0x26BD, 0x26BE}, {0x26C4, 0x26C5},
{0x26CE, 0x26CE}, {0x26D4, 0x26D4}, {0x26EA, 0x26EA},
{0x26F2, 0x26F3}, {0x26F5, 0x26F5}, {0x26FA, 0x26FA},
{0x26FD, 0x26FD}, {0x2705, 0x2705}, {0x270A, 0x270B},
{0x2728, 0x2728}, {0x274C, 0x274C}, {0x274E, 0x274E},
{0x2753, 0x2755}, {0x2757, 0x2757}, {0x2795, 0x2797},
{0x27B0, 0x27B0}, {0x27BF, 0x27BF}, {0x2B1B, 0x2B1C},
{0x2B50, 0x2B50}, {0x2B55, 0x2B55}, {0x2E80, 0x2E99},
{0x2E9B, 0x2EF3}, {0x2F00, 0x2FD5}, {0x2FF0, 0x2FFB},
{0x3000, 0x303E}, {0x3041, 0x3096}, {0x3099, 0x30FF},
{0x3105, 0x312F}, {0x3131, 0x318E}, {0x3190, 0x31BA},
{0x31C0, 0x31E3}, {0x31F0, 0x321E}, {0x3220, 0x3247},
{0x3250, 0x4DBF}, {0x4E00, 0xA48C}, {0xA490, 0xA4C6},
{0xA960, 0xA97C}, {0xAC00, 0xD7A3}, {0xF900, 0xFAFF},
{0xFE10, 0xFE19}, {0xFE30, 0xFE52}, {0xFE54, 0xFE66},
{0xFE68, 0xFE6B}, {0xFF01, 0xFF60}, {0xFFE0, 0xFFE6},
{0x16FE0, 0x16FE3}, {0x17000, 0x187F7}, {0x18800, 0x18AF2},
{0x1B000, 0x1B11E}, {0x1B150, 0x1B152}, {0x1B164, 0x1B167},
{0x1B170, 0x1B2FB}, {0x1F004, 0x1F004}, {0x1F0CF, 0x1F0CF},
{0x1F18E, 0x1F18E}, {0x1F191, 0x1F19A}, {0x1F200, 0x1F202},
{0x1F210, 0x1F23B}, {0x1F240, 0x1F248}, {0x1F250, 0x1F251},
{0x1F260, 0x1F265}, {0x1F300, 0x1F320}, {0x1F32D, 0x1F335},
{0x1F337, 0x1F37C}, {0x1F37E, 0x1F393}, {0x1F3A0, 0x1F3CA},
{0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0}, {0x1F3F4, 0x1F3F4},
{0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440}, {0x1F442, 0x1F4FC},
{0x1F4FF, 0x1F53D}, {0x1F54B, 0x1F54E}, {0x1F550, 0x1F567},
{0x1F57A, 0x1F57A}, {0x1F595, 0x1F596}, {0x1F5A4, 0x1F5A4},
{0x1F5FB, 0x1F64F}, {0x1F680, 0x1F6C5}, {0x1F6CC, 0x1F6CC},
{0x1F6D0, 0x1F6D2}, {0x1F6D5, 0x1F6D5}, {0x1F6EB, 0x1F6EC},
{0x1F6F4, 0x1F6FA}, {0x1F7E0, 0x1F7EB}, {0x1F90D, 0x1F971},
{0x1F973, 0x1F976}, {0x1F97A, 0x1F9A2}, {0x1F9A5, 0x1F9AA},
{0x1F9AE, 0x1F9CA}, {0x1F9CD, 0x1F9FF}, {0x1FA70, 0x1FA73},
{0x1FA78, 0x1FA7A}, {0x1FA80, 0x1FA82}, {0x1FA90, 0x1FA95},
{0x20000, 0x2FFFD}, {0x30000, 0x3FFFD},
}
var ambiguous = table{
{0x00A1, 0x00A1}, {0x00A4, 0x00A4}, {0x00A7, 0x00A8},
{0x00AA, 0x00AA}, {0x00AD, 0x00AE}, {0x00B0, 0x00B4},
{0x00B6, 0x00BA}, {0x00BC, 0x00BF}, {0x00C6, 0x00C6},
{0x00D0, 0x00D0}, {0x00D7, 0x00D8}, {0x00DE, 0x00E1},
{0x00E6, 0x00E6}, {0x00E8, 0x00EA}, {0x00EC, 0x00ED},
{0x00F0, 0x00F0}, {0x00F2, 0x00F3}, {0x00F7, 0x00FA},
{0x00FC, 0x00FC}, {0x00FE, 0x00FE}, {0x0101, 0x0101},
{0x0111, 0x0111}, {0x0113, 0x0113}, {0x011B, 0x011B},
{0x0126, 0x0127}, {0x012B, 0x012B}, {0x0131, 0x0133},
{0x0138, 0x0138}, {0x013F, 0x0142}, {0x0144, 0x0144},
{0x0148, 0x014B}, {0x014D, 0x014D}, {0x0152, 0x0153},
{0x0166, 0x0167}, {0x016B, 0x016B}, {0x01CE, 0x01CE},
{0x01D0, 0x01D0}, {0x01D2, 0x01D2}, {0x01D4, 0x01D4},
{0x01D6, 0x01D6}, {0x01D8, 0x01D8}, {0x01DA, 0x01DA},
{0x01DC, 0x01DC}, {0x0251, 0x0251}, {0x0261, 0x0261},
{0x02C4, 0x02C4}, {0x02C7, 0x02C7}, {0x02C9, 0x02CB},
{0x02CD, 0x02CD}, {0x02D0, 0x02D0}, {0x02D8, 0x02DB},
{0x02DD, 0x02DD}, {0x02DF, 0x02DF}, {0x0300, 0x036F},
{0x0391, 0x03A1}, {0x03A3, 0x03A9}, {0x03B1, 0x03C1},
{0x03C3, 0x03C9}, {0x0401, 0x0401}, {0x0410, 0x044F},
{0x0451, 0x0451}, {0x2010, 0x2010}, {0x2013, 0x2016},
{0x2018, 0x2019}, {0x201C, 0x201D}, {0x2020, 0x2022},
{0x2024, 0x2027}, {0x2030, 0x2030}, {0x2032, 0x2033},
{0x2035, 0x2035}, {0x203B, 0x203B}, {0x203E, 0x203E},
{0x2074, 0x2074}, {0x207F, 0x207F}, {0x2081, 0x2084},
{0x20AC, 0x20AC}, {0x2103, 0x2103}, {0x2105, 0x2105},
{0x2109, 0x2109}, {0x2113, 0x2113}, {0x2116, 0x2116},
{0x2121, 0x2122}, {0x2126, 0x2126}, {0x212B, 0x212B},
{0x2153, 0x2154}, {0x215B, 0x215E}, {0x2160, 0x216B},
{0x2170, 0x2179}, {0x2189, 0x2189}, {0x2190, 0x2199},
{0x21B8, 0x21B9}, {0x21D2, 0x21D2}, {0x21D4, 0x21D4},
{0x21E7, 0x21E7}, {0x2200, 0x2200}, {0x2202, 0x2203},
{0x2207, 0x2208}, {0x220B, 0x220B}, {0x220F, 0x220F},
{0x2211, 0x2211}, {0x2215, 0x2215}, {0x221A, 0x221A},
{0x221D, 0x2220}, {0x2223, 0x2223}, {0x2225, 0x2225},
{0x2227, 0x222C}, {0x222E, 0x222E}, {0x2234, 0x2237},
{0x223C, 0x223D}, {0x2248, 0x2248}, {0x224C, 0x224C},
{0x2252, 0x2252}, {0x2260, 0x2261}, {0x2264, 0x2267},
{0x226A, 0x226B}, {0x226E, 0x226F}, {0x2282, 0x2283},
{0x2286, 0x2287}, {0x2295, 0x2295}, {0x2299, 0x2299},
{0x22A5, 0x22A5}, {0x22BF, 0x22BF}, {0x2312, 0x2312},
{0x2460, 0x24E9}, {0x24EB, 0x254B}, {0x2550, 0x2573},
{0x2580, 0x258F}, {0x2592, 0x2595}, {0x25A0, 0x25A1},
{0x25A3, 0x25A9}, {0x25B2, 0x25B3}, {0x25B6, 0x25B7},
{0x25BC, 0x25BD}, {0x25C0, 0x25C1}, {0x25C6, 0x25C8},
{0x25CB, 0x25CB}, {0x25CE, 0x25D1}, {0x25E2, 0x25E5},
{0x25EF, 0x25EF}, {0x2605, 0x2606}, {0x2609, 0x2609},
{0x260E, 0x260F}, {0x261C, 0x261C}, {0x261E, 0x261E},
{0x2640, 0x2640}, {0x2642, 0x2642}, {0x2660, 0x2661},
{0x2663, 0x2665}, {0x2667, 0x266A}, {0x266C, 0x266D},
{0x266F, 0x266F}, {0x269E, 0x269F}, {0x26BF, 0x26BF},
{0x26C6, 0x26CD}, {0x26CF, 0x26D3}, {0x26D5, 0x26E1},
{0x26E3, 0x26E3}, {0x26E8, 0x26E9}, {0x26EB, 0x26F1},
{0x26F4, 0x26F4}, {0x26F6, 0x26F9}, {0x26FB, 0x26FC},
{0x26FE, 0x26FF}, {0x273D, 0x273D}, {0x2776, 0x277F},
{0x2B56, 0x2B59}, {0x3248, 0x324F}, {0xE000, 0xF8FF},
{0xFE00, 0xFE0F}, {0xFFFD, 0xFFFD}, {0x1F100, 0x1F10A},
{0x1F110, 0x1F12D}, {0x1F130, 0x1F169}, {0x1F170, 0x1F18D},
{0x1F18F, 0x1F190}, {0x1F19B, 0x1F1AC}, {0xE0100, 0xE01EF},
{0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD},
}
var notassigned = table{
{0x27E6, 0x27ED}, {0x2985, 0x2986},
}
var neutral = table{
{0x0000, 0x001F}, {0x007F, 0x00A0}, {0x00A9, 0x00A9},
{0x00AB, 0x00AB}, {0x00B5, 0x00B5}, {0x00BB, 0x00BB},
{0x00C0, 0x00C5}, {0x00C7, 0x00CF}, {0x00D1, 0x00D6},
{0x00D9, 0x00DD}, {0x00E2, 0x00E5}, {0x00E7, 0x00E7},
{0x00EB, 0x00EB}, {0x00EE, 0x00EF}, {0x00F1, 0x00F1},
{0x00F4, 0x00F6}, {0x00FB, 0x00FB}, {0x00FD, 0x00FD},
{0x00FF, 0x0100}, {0x0102, 0x0110}, {0x0112, 0x0112},
{0x0114, 0x011A}, {0x011C, 0x0125}, {0x0128, 0x012A},
{0x012C, 0x0130}, {0x0134, 0x0137}, {0x0139, 0x013E},
{0x0143, 0x0143}, {0x0145, 0x0147}, {0x014C, 0x014C},
{0x014E, 0x0151}, {0x0154, 0x0165}, {0x0168, 0x016A},
{0x016C, 0x01CD}, {0x01CF, 0x01CF}, {0x01D1, 0x01D1},
{0x01D3, 0x01D3}, {0x01D5, 0x01D5}, {0x01D7, 0x01D7},
{0x01D9, 0x01D9}, {0x01DB, 0x01DB}, {0x01DD, 0x0250},
{0x0252, 0x0260}, {0x0262, 0x02C3}, {0x02C5, 0x02C6},
{0x02C8, 0x02C8}, {0x02CC, 0x02CC}, {0x02CE, 0x02CF},
{0x02D1, 0x02D7}, {0x02DC, 0x02DC}, {0x02DE, 0x02DE},
{0x02E0, 0x02FF}, {0x0370, 0x0377}, {0x037A, 0x037F},
{0x0384, 0x038A}, {0x038C, 0x038C}, {0x038E, 0x0390},
{0x03AA, 0x03B0}, {0x03C2, 0x03C2}, {0x03CA, 0x0400},
{0x0402, 0x040F}, {0x0450, 0x0450}, {0x0452, 0x052F},
{0x0531, 0x0556}, {0x0559, 0x058A}, {0x058D, 0x058F},
{0x0591, 0x05C7}, {0x05D0, 0x05EA}, {0x05EF, 0x05F4},
{0x0600, 0x061C}, {0x061E, 0x070D}, {0x070F, 0x074A},
{0x074D, 0x07B1}, {0x07C0, 0x07FA}, {0x07FD, 0x082D},
{0x0830, 0x083E}, {0x0840, 0x085B}, {0x085E, 0x085E},
{0x0860, 0x086A}, {0x08A0, 0x08B4}, {0x08B6, 0x08BD},
{0x08D3, 0x0983}, {0x0985, 0x098C}, {0x098F, 0x0990},
{0x0993, 0x09A8}, {0x09AA, 0x09B0}, {0x09B2, 0x09B2},
{0x09B6, 0x09B9}, {0x09BC, 0x09C4}, {0x09C7, 0x09C8},
{0x09CB, 0x09CE}, {0x09D7, 0x09D7}, {0x09DC, 0x09DD},
{0x09DF, 0x09E3}, {0x09E6, 0x09FE}, {0x0A01, 0x0A03},
{0x0A05, 0x0A0A}, {0x0A0F, 0x0A10}, {0x0A13, 0x0A28},
{0x0A2A, 0x0A30}, {0x0A32, 0x0A33}, {0x0A35, 0x0A36},
{0x0A38, 0x0A39}, {0x0A3C, 0x0A3C}, {0x0A3E, 0x0A42},
{0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, {0x0A51, 0x0A51},
{0x0A59, 0x0A5C}, {0x0A5E, 0x0A5E}, {0x0A66, 0x0A76},
{0x0A81, 0x0A83}, {0x0A85, 0x0A8D}, {0x0A8F, 0x0A91},
{0x0A93, 0x0AA8}, {0x0AAA, 0x0AB0}, {0x0AB2, 0x0AB3},
{0x0AB5, 0x0AB9}, {0x0ABC, 0x0AC5}, {0x0AC7, 0x0AC9},
{0x0ACB, 0x0ACD}, {0x0AD0, 0x0AD0}, {0x0AE0, 0x0AE3},
{0x0AE6, 0x0AF1}, {0x0AF9, 0x0AFF}, {0x0B01, 0x0B03},
{0x0B05, 0x0B0C}, {0x0B0F, 0x0B10}, {0x0B13, 0x0B28},
{0x0B2A, 0x0B30}, {0x0B32, 0x0B33}, {0x0B35, 0x0B39},
{0x0B3C, 0x0B44}, {0x0B47, 0x0B48}, {0x0B4B, 0x0B4D},
{0x0B56, 0x0B57}, {0x0B5C, 0x0B5D}, {0x0B5F, 0x0B63},
{0x0B66, 0x0B77}, {0x0B82, 0x0B83}, {0x0B85, 0x0B8A},
{0x0B8E, 0x0B90}, {0x0B92, 0x0B95}, {0x0B99, 0x0B9A},
{0x0B9C, 0x0B9C}, {0x0B9E, 0x0B9F}, {0x0BA3, 0x0BA4},
{0x0BA8, 0x0BAA}, {0x0BAE, 0x0BB9}, {0x0BBE, 0x0BC2},
{0x0BC6, 0x0BC8}, {0x0BCA, 0x0BCD}, {0x0BD0, 0x0BD0},
{0x0BD7, 0x0BD7}, {0x0BE6, 0x0BFA}, {0x0C00, 0x0C0C},
{0x0C0E, 0x0C10}, {0x0C12, 0x0C28}, {0x0C2A, 0x0C39},
{0x0C3D, 0x0C44}, {0x0C46, 0x0C48}, {0x0C4A, 0x0C4D},
{0x0C55, 0x0C56}, {0x0C58, 0x0C5A}, {0x0C60, 0x0C63},
{0x0C66, 0x0C6F}, {0x0C77, 0x0C8C}, {0x0C8E, 0x0C90},
{0x0C92, 0x0CA8}, {0x0CAA, 0x0CB3}, {0x0CB5, 0x0CB9},
{0x0CBC, 0x0CC4}, {0x0CC6, 0x0CC8}, {0x0CCA, 0x0CCD},
{0x0CD5, 0x0CD6}, {0x0CDE, 0x0CDE}, {0x0CE0, 0x0CE3},
{0x0CE6, 0x0CEF}, {0x0CF1, 0x0CF2}, {0x0D00, 0x0D03},
{0x0D05, 0x0D0C}, {0x0D0E, 0x0D10}, {0x0D12, 0x0D44},
{0x0D46, 0x0D48}, {0x0D4A, 0x0D4F}, {0x0D54, 0x0D63},
{0x0D66, 0x0D7F}, {0x0D82, 0x0D83}, {0x0D85, 0x0D96},
{0x0D9A, 0x0DB1}, {0x0DB3, 0x0DBB}, {0x0DBD, 0x0DBD},
{0x0DC0, 0x0DC6}, {0x0DCA, 0x0DCA}, {0x0DCF, 0x0DD4},
{0x0DD6, 0x0DD6}, {0x0DD8, 0x0DDF}, {0x0DE6, 0x0DEF},
{0x0DF2, 0x0DF4}, {0x0E01, 0x0E3A}, {0x0E3F, 0x0E5B},
{0x0E81, 0x0E82}, {0x0E84, 0x0E84}, {0x0E86, 0x0E8A},
{0x0E8C, 0x0EA3}, {0x0EA5, 0x0EA5}, {0x0EA7, 0x0EBD},
{0x0EC0, 0x0EC4}, {0x0EC6, 0x0EC6}, {0x0EC8, 0x0ECD},
{0x0ED0, 0x0ED9}, {0x0EDC, 0x0EDF}, {0x0F00, 0x0F47},
{0x0F49, 0x0F6C}, {0x0F71, 0x0F97}, {0x0F99, 0x0FBC},
{0x0FBE, 0x0FCC}, {0x0FCE, 0x0FDA}, {0x1000, 0x10C5},
{0x10C7, 0x10C7}, {0x10CD, 0x10CD}, {0x10D0, 0x10FF},
{0x1160, 0x1248}, {0x124A, 0x124D}, {0x1250, 0x1256},
{0x1258, 0x1258}, {0x125A, 0x125D}, {0x1260, 0x1288},
{0x128A, 0x128D}, {0x1290, 0x12B0}, {0x12B2, 0x12B5},
{0x12B8, 0x12BE}, {0x12C0, 0x12C0}, {0x12C2, 0x12C5},
{0x12C8, 0x12D6}, {0x12D8, 0x1310}, {0x1312, 0x1315},
{0x1318, 0x135A}, {0x135D, 0x137C}, {0x1380, 0x1399},
{0x13A0, 0x13F5}, {0x13F8, 0x13FD}, {0x1400, 0x169C},
{0x16A0, 0x16F8}, {0x1700, 0x170C}, {0x170E, 0x1714},
{0x1720, 0x1736}, {0x1740, 0x1753}, {0x1760, 0x176C},
{0x176E, 0x1770}, {0x1772, 0x1773}, {0x1780, 0x17DD},
{0x17E0, 0x17E9}, {0x17F0, 0x17F9}, {0x1800, 0x180E},
{0x1810, 0x1819}, {0x1820, 0x1878}, {0x1880, 0x18AA},
{0x18B0, 0x18F5}, {0x1900, 0x191E}, {0x1920, 0x192B},
{0x1930, 0x193B}, {0x1940, 0x1940}, {0x1944, 0x196D},
{0x1970, 0x1974}, {0x1980, 0x19AB}, {0x19B0, 0x19C9},
{0x19D0, 0x19DA}, {0x19DE, 0x1A1B}, {0x1A1E, 0x1A5E},
{0x1A60, 0x1A7C}, {0x1A7F, 0x1A89}, {0x1A90, 0x1A99},
{0x1AA0, 0x1AAD}, {0x1AB0, 0x1ABE}, {0x1B00, 0x1B4B},
{0x1B50, 0x1B7C}, {0x1B80, 0x1BF3}, {0x1BFC, 0x1C37},
{0x1C3B, 0x1C49}, {0x1C4D, 0x1C88}, {0x1C90, 0x1CBA},
{0x1CBD, 0x1CC7}, {0x1CD0, 0x1CFA}, {0x1D00, 0x1DF9},
{0x1DFB, 0x1F15}, {0x1F18, 0x1F1D}, {0x1F20, 0x1F45},
{0x1F48, 0x1F4D}, {0x1F50, 0x1F57}, {0x1F59, 0x1F59},
{0x1F5B, 0x1F5B}, {0x1F5D, 0x1F5D}, {0x1F5F, 0x1F7D},
{0x1F80, 0x1FB4}, {0x1FB6, 0x1FC4}, {0x1FC6, 0x1FD3},
{0x1FD6, 0x1FDB}, {0x1FDD, 0x1FEF}, {0x1FF2, 0x1FF4},
{0x1FF6, 0x1FFE}, {0x2000, 0x200F}, {0x2011, 0x2012},
{0x2017, 0x2017}, {0x201A, 0x201B}, {0x201E, 0x201F},
{0x2023, 0x2023}, {0x2028, 0x202F}, {0x2031, 0x2031},
{0x2034, 0x2034}, {0x2036, 0x203A}, {0x203C, 0x203D},
{0x203F, 0x2064}, {0x2066, 0x2071}, {0x2075, 0x207E},
{0x2080, 0x2080}, {0x2085, 0x208E}, {0x2090, 0x209C},
{0x20A0, 0x20A8}, {0x20AA, 0x20AB}, {0x20AD, 0x20BF},
{0x20D0, 0x20F0}, {0x2100, 0x2102}, {0x2104, 0x2104},
{0x2106, 0x2108}, {0x210A, 0x2112}, {0x2114, 0x2115},
{0x2117, 0x2120}, {0x2123, 0x2125}, {0x2127, 0x212A},
{0x212C, 0x2152}, {0x2155, 0x215A}, {0x215F, 0x215F},
{0x216C, 0x216F}, {0x217A, 0x2188}, {0x218A, 0x218B},
{0x219A, 0x21B7}, {0x21BA, 0x21D1}, {0x21D3, 0x21D3},
{0x21D5, 0x21E6}, {0x21E8, 0x21FF}, {0x2201, 0x2201},
{0x2204, 0x2206}, {0x2209, 0x220A}, {0x220C, 0x220E},
{0x2210, 0x2210}, {0x2212, 0x2214}, {0x2216, 0x2219},
{0x221B, 0x221C}, {0x2221, 0x2222}, {0x2224, 0x2224},
{0x2226, 0x2226}, {0x222D, 0x222D}, {0x222F, 0x2233},
{0x2238, 0x223B}, {0x223E, 0x2247}, {0x2249, 0x224B},
{0x224D, 0x2251}, {0x2253, 0x225F}, {0x2262, 0x2263},
{0x2268, 0x2269}, {0x226C, 0x226D}, {0x2270, 0x2281},
{0x2284, 0x2285}, {0x2288, 0x2294}, {0x2296, 0x2298},
{0x229A, 0x22A4}, {0x22A6, 0x22BE}, {0x22C0, 0x2311},
{0x2313, 0x2319}, {0x231C, 0x2328}, {0x232B, 0x23E8},
{0x23ED, 0x23EF}, {0x23F1, 0x23F2}, {0x23F4, 0x2426},
{0x2440, 0x244A}, {0x24EA, 0x24EA}, {0x254C, 0x254F},
{0x2574, 0x257F}, {0x2590, 0x2591}, {0x2596, 0x259F},
{0x25A2, 0x25A2}, {0x25AA, 0x25B1}, {0x25B4, 0x25B5},
{0x25B8, 0x25BB}, {0x25BE, 0x25BF}, {0x25C2, 0x25C5},
{0x25C9, 0x25CA}, {0x25CC, 0x25CD}, {0x25D2, 0x25E1},
{0x25E6, 0x25EE}, {0x25F0, 0x25FC}, {0x25FF, 0x2604},
{0x2607, 0x2608}, {0x260A, 0x260D}, {0x2610, 0x2613},
{0x2616, 0x261B}, {0x261D, 0x261D}, {0x261F, 0x263F},
{0x2641, 0x2641}, {0x2643, 0x2647}, {0x2654, 0x265F},
{0x2662, 0x2662}, {0x2666, 0x2666}, {0x266B, 0x266B},
{0x266E, 0x266E}, {0x2670, 0x267E}, {0x2680, 0x2692},
{0x2694, 0x269D}, {0x26A0, 0x26A0}, {0x26A2, 0x26A9},
{0x26AC, 0x26BC}, {0x26C0, 0x26C3}, {0x26E2, 0x26E2},
{0x26E4, 0x26E7}, {0x2700, 0x2704}, {0x2706, 0x2709},
{0x270C, 0x2727}, {0x2729, 0x273C}, {0x273E, 0x274B},
{0x274D, 0x274D}, {0x274F, 0x2752}, {0x2756, 0x2756},
{0x2758, 0x2775}, {0x2780, 0x2794}, {0x2798, 0x27AF},
{0x27B1, 0x27BE}, {0x27C0, 0x27E5}, {0x27EE, 0x2984},
{0x2987, 0x2B1A}, {0x2B1D, 0x2B4F}, {0x2B51, 0x2B54},
{0x2B5A, 0x2B73}, {0x2B76, 0x2B95}, {0x2B98, 0x2C2E},
{0x2C30, 0x2C5E}, {0x2C60, 0x2CF3}, {0x2CF9, 0x2D25},
{0x2D27, 0x2D27}, {0x2D2D, 0x2D2D}, {0x2D30, 0x2D67},
{0x2D6F, 0x2D70}, {0x2D7F, 0x2D96}, {0x2DA0, 0x2DA6},
{0x2DA8, 0x2DAE}, {0x2DB0, 0x2DB6}, {0x2DB8, 0x2DBE},
{0x2DC0, 0x2DC6}, {0x2DC8, 0x2DCE}, {0x2DD0, 0x2DD6},
{0x2DD8, 0x2DDE}, {0x2DE0, 0x2E4F}, {0x303F, 0x303F},
{0x4DC0, 0x4DFF}, {0xA4D0, 0xA62B}, {0xA640, 0xA6F7},
{0xA700, 0xA7BF}, {0xA7C2, 0xA7C6}, {0xA7F7, 0xA82B},
{0xA830, 0xA839}, {0xA840, 0xA877}, {0xA880, 0xA8C5},
{0xA8CE, 0xA8D9}, {0xA8E0, 0xA953}, {0xA95F, 0xA95F},
{0xA980, 0xA9CD}, {0xA9CF, 0xA9D9}, {0xA9DE, 0xA9FE},
{0xAA00, 0xAA36}, {0xAA40, 0xAA4D}, {0xAA50, 0xAA59},
{0xAA5C, 0xAAC2}, {0xAADB, 0xAAF6}, {0xAB01, 0xAB06},
{0xAB09, 0xAB0E}, {0xAB11, 0xAB16}, {0xAB20, 0xAB26},
{0xAB28, 0xAB2E}, {0xAB30, 0xAB67}, {0xAB70, 0xABED},
{0xABF0, 0xABF9}, {0xD7B0, 0xD7C6}, {0xD7CB, 0xD7FB},
{0xD800, 0xDFFF}, {0xFB00, 0xFB06}, {0xFB13, 0xFB17},
{0xFB1D, 0xFB36}, {0xFB38, 0xFB3C}, {0xFB3E, 0xFB3E},
{0xFB40, 0xFB41}, {0xFB43, 0xFB44}, {0xFB46, 0xFBC1},
{0xFBD3, 0xFD3F}, {0xFD50, 0xFD8F}, {0xFD92, 0xFDC7},
{0xFDF0, 0xFDFD}, {0xFE20, 0xFE2F}, {0xFE70, 0xFE74},
{0xFE76, 0xFEFC}, {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFC},
{0x10000, 0x1000B}, {0x1000D, 0x10026}, {0x10028, 0x1003A},
{0x1003C, 0x1003D}, {0x1003F, 0x1004D}, {0x10050, 0x1005D},
{0x10080, 0x100FA}, {0x10100, 0x10102}, {0x10107, 0x10133},
{0x10137, 0x1018E}, {0x10190, 0x1019B}, {0x101A0, 0x101A0},
{0x101D0, 0x101FD}, {0x10280, 0x1029C}, {0x102A0, 0x102D0},
{0x102E0, 0x102FB}, {0x10300, 0x10323}, {0x1032D, 0x1034A},
{0x10350, 0x1037A}, {0x10380, 0x1039D}, {0x1039F, 0x103C3},
{0x103C8, 0x103D5}, {0x10400, 0x1049D}, {0x104A0, 0x104A9},
{0x104B0, 0x104D3}, {0x104D8, 0x104FB}, {0x10500, 0x10527},
{0x10530, 0x10563}, {0x1056F, 0x1056F}, {0x10600, 0x10736},
{0x10740, 0x10755}, {0x10760, 0x10767}, {0x10800, 0x10805},
{0x10808, 0x10808}, {0x1080A, 0x10835}, {0x10837, 0x10838},
{0x1083C, 0x1083C}, {0x1083F, 0x10855}, {0x10857, 0x1089E},
{0x108A7, 0x108AF}, {0x108E0, 0x108F2}, {0x108F4, 0x108F5},
{0x108FB, 0x1091B}, {0x1091F, 0x10939}, {0x1093F, 0x1093F},
{0x10980, 0x109B7}, {0x109BC, 0x109CF}, {0x109D2, 0x10A03},
{0x10A05, 0x10A06}, {0x10A0C, 0x10A13}, {0x10A15, 0x10A17},
{0x10A19, 0x10A35}, {0x10A38, 0x10A3A}, {0x10A3F, 0x10A48},
{0x10A50, 0x10A58}, {0x10A60, 0x10A9F}, {0x10AC0, 0x10AE6},
{0x10AEB, 0x10AF6}, {0x10B00, 0x10B35}, {0x10B39, 0x10B55},
{0x10B58, 0x10B72}, {0x10B78, 0x10B91}, {0x10B99, 0x10B9C},
{0x10BA9, 0x10BAF}, {0x10C00, 0x10C48}, {0x10C80, 0x10CB2},
{0x10CC0, 0x10CF2}, {0x10CFA, 0x10D27}, {0x10D30, 0x10D39},
{0x10E60, 0x10E7E}, {0x10F00, 0x10F27}, {0x10F30, 0x10F59},
{0x10FE0, 0x10FF6}, {0x11000, 0x1104D}, {0x11052, 0x1106F},
{0x1107F, 0x110C1}, {0x110CD, 0x110CD}, {0x110D0, 0x110E8},
{0x110F0, 0x110F9}, {0x11100, 0x11134}, {0x11136, 0x11146},
{0x11150, 0x11176}, {0x11180, 0x111CD}, {0x111D0, 0x111DF},
{0x111E1, 0x111F4}, {0x11200, 0x11211}, {0x11213, 0x1123E},
{0x11280, 0x11286}, {0x11288, 0x11288}, {0x1128A, 0x1128D},
{0x1128F, 0x1129D}, {0x1129F, 0x112A9}, {0x112B0, 0x112EA},
{0x112F0, 0x112F9}, {0x11300, 0x11303}, {0x11305, 0x1130C},
{0x1130F, 0x11310}, {0x11313, 0x11328}, {0x1132A, 0x11330},
{0x11332, 0x11333}, {0x11335, 0x11339}, {0x1133B, 0x11344},
{0x11347, 0x11348}, {0x1134B, 0x1134D}, {0x11350, 0x11350},
{0x11357, 0x11357}, {0x1135D, 0x11363}, {0x11366, 0x1136C},
{0x11370, 0x11374}, {0x11400, 0x11459}, {0x1145B, 0x1145B},
{0x1145D, 0x1145F}, {0x11480, 0x114C7}, {0x114D0, 0x114D9},
{0x11580, 0x115B5}, {0x115B8, 0x115DD}, {0x11600, 0x11644},
{0x11650, 0x11659}, {0x11660, 0x1166C}, {0x11680, 0x116B8},
{0x116C0, 0x116C9}, {0x11700, 0x1171A}, {0x1171D, 0x1172B},
{0x11730, 0x1173F}, {0x11800, 0x1183B}, {0x118A0, 0x118F2},
{0x118FF, 0x118FF}, {0x119A0, 0x119A7}, {0x119AA, 0x119D7},
{0x119DA, 0x119E4}, {0x11A00, 0x11A47}, {0x11A50, 0x11AA2},
{0x11AC0, 0x11AF8}, {0x11C00, 0x11C08}, {0x11C0A, 0x11C36},
{0x11C38, 0x11C45}, {0x11C50, 0x11C6C}, {0x11C70, 0x11C8F},
{0x11C92, 0x11CA7}, {0x11CA9, 0x11CB6}, {0x11D00, 0x11D06},
{0x11D08, 0x11D09}, {0x11D0B, 0x11D36}, {0x11D3A, 0x11D3A},
{0x11D3C, 0x11D3D}, {0x11D3F, 0x11D47}, {0x11D50, 0x11D59},
{0x11D60, 0x11D65}, {0x11D67, 0x11D68}, {0x11D6A, 0x11D8E},
{0x11D90, 0x11D91}, {0x11D93, 0x11D98}, {0x11DA0, 0x11DA9},
{0x11EE0, 0x11EF8}, {0x11FC0, 0x11FF1}, {0x11FFF, 0x12399},
{0x12400, 0x1246E}, {0x12470, 0x12474}, {0x12480, 0x12543},
{0x13000, 0x1342E}, {0x13430, 0x13438}, {0x14400, 0x14646},
{0x16800, 0x16A38}, {0x16A40, 0x16A5E}, {0x16A60, 0x16A69},
{0x16A6E, 0x16A6F}, {0x16AD0, 0x16AED}, {0x16AF0, 0x16AF5},
{0x16B00, 0x16B45}, {0x16B50, 0x16B59}, {0x16B5B, 0x16B61},
{0x16B63, 0x16B77}, {0x16B7D, 0x16B8F}, {0x16E40, 0x16E9A},
{0x16F00, 0x16F4A}, {0x16F4F, 0x16F87}, {0x16F8F, 0x16F9F},
{0x1BC00, 0x1BC6A}, {0x1BC70, 0x1BC7C}, {0x1BC80, 0x1BC88},
{0x1BC90, 0x1BC99}, {0x1BC9C, 0x1BCA3}, {0x1D000, 0x1D0F5},
{0x1D100, 0x1D126}, {0x1D129, 0x1D1E8}, {0x1D200, 0x1D245},
{0x1D2E0, 0x1D2F3}, {0x1D300, 0x1D356}, {0x1D360, 0x1D378},
{0x1D400, 0x1D454}, {0x1D456, 0x1D49C}, {0x1D49E, 0x1D49F},
{0x1D4A2, 0x1D4A2}, {0x1D4A5, 0x1D4A6}, {0x1D4A9, 0x1D4AC},
{0x1D4AE, 0x1D4B9}, {0x1D4BB, 0x1D4BB}, {0x1D4BD, 0x1D4C3},
{0x1D4C5, 0x1D505}, {0x1D507, 0x1D50A}, {0x1D50D, 0x1D514},
{0x1D516, 0x1D51C}, {0x1D51E, 0x1D539}, {0x1D53B, 0x1D53E},
{0x1D540, 0x1D544}, {0x1D546, 0x1D546}, {0x1D54A, 0x1D550},
{0x1D552, 0x1D6A5}, {0x1D6A8, 0x1D7CB}, {0x1D7CE, 0x1DA8B},
{0x1DA9B, 0x1DA9F}, {0x1DAA1, 0x1DAAF}, {0x1E000, 0x1E006},
{0x1E008, 0x1E018}, {0x1E01B, 0x1E021}, {0x1E023, 0x1E024},
{0x1E026, 0x1E02A}, {0x1E100, 0x1E12C}, {0x1E130, 0x1E13D},
{0x1E140, 0x1E149}, {0x1E14E, 0x1E14F}, {0x1E2C0, 0x1E2F9},
{0x1E2FF, 0x1E2FF}, {0x1E800, 0x1E8C4}, {0x1E8C7, 0x1E8D6},
{0x1E900, 0x1E94B}, {0x1E950, 0x1E959}, {0x1E95E, 0x1E95F},
{0x1EC71, 0x1ECB4}, {0x1ED01, 0x1ED3D}, {0x1EE00, 0x1EE03},
{0x1EE05, 0x1EE1F}, {0x1EE21, 0x1EE22}, {0x1EE24, 0x1EE24},
{0x1EE27, 0x1EE27}, {0x1EE29, 0x1EE32}, {0x1EE34, 0x1EE37},
{0x1EE39, 0x1EE39}, {0x1EE3B, 0x1EE3B}, {0x1EE42, 0x1EE42},
{0x1EE47, 0x1EE47}, {0x1EE49, 0x1EE49}, {0x1EE4B, 0x1EE4B},
{0x1EE4D, 0x1EE4F}, {0x1EE51, 0x1EE52}, {0x1EE54, 0x1EE54},
{0x1EE57, 0x1EE57}, {0x1EE59, 0x1EE59}, {0x1EE5B, 0x1EE5B},
{0x1EE5D, 0x1EE5D}, {0x1EE5F, 0x1EE5F}, {0x1EE61, 0x1EE62},
{0x1EE64, 0x1EE64}, {0x1EE67, 0x1EE6A}, {0x1EE6C, 0x1EE72},
{0x1EE74, 0x1EE77}, {0x1EE79, 0x1EE7C}, {0x1EE7E, 0x1EE7E},
{0x1EE80, 0x1EE89}, {0x1EE8B, 0x1EE9B}, {0x1EEA1, 0x1EEA3},
{0x1EEA5, 0x1EEA9}, {0x1EEAB, 0x1EEBB}, {0x1EEF0, 0x1EEF1},
{0x1F000, 0x1F003}, {0x1F005, 0x1F02B}, {0x1F030, 0x1F093},
{0x1F0A0, 0x1F0AE}, {0x1F0B1, 0x1F0BF}, {0x1F0C1, 0x1F0CE},
{0x1F0D1, 0x1F0F5}, {0x1F10B, 0x1F10C}, {0x1F12E, 0x1F12F},
{0x1F16A, 0x1F16C}, {0x1F1E6, 0x1F1FF}, {0x1F321, 0x1F32C},
{0x1F336, 0x1F336}, {0x1F37D, 0x1F37D}, {0x1F394, 0x1F39F},
{0x1F3CB, 0x1F3CE}, {0x1F3D4, 0x1F3DF}, {0x1F3F1, 0x1F3F3},
{0x1F3F5, 0x1F3F7}, {0x1F43F, 0x1F43F}, {0x1F441, 0x1F441},
{0x1F4FD, 0x1F4FE}, {0x1F53E, 0x1F54A}, {0x1F54F, 0x1F54F},
{0x1F568, 0x1F579}, {0x1F57B, 0x1F594}, {0x1F597, 0x1F5A3},
{0x1F5A5, 0x1F5FA}, {0x1F650, 0x1F67F}, {0x1F6C6, 0x1F6CB},
{0x1F6CD, 0x1F6CF}, {0x1F6D3, 0x1F6D4}, {0x1F6E0, 0x1F6EA},
{0x1F6F0, 0x1F6F3}, {0x1F700, 0x1F773}, {0x1F780, 0x1F7D8},
{0x1F800, 0x1F80B}, {0x1F810, 0x1F847}, {0x1F850, 0x1F859},
{0x1F860, 0x1F887}, {0x1F890, 0x1F8AD}, {0x1F900, 0x1F90B},
{0x1FA00, 0x1FA53}, {0x1FA60, 0x1FA6D}, {0xE0001, 0xE0001},
{0xE0020, 0xE007F},
}
var emoji = table{
{0x203C, 0x203C}, {0x2049, 0x2049}, {0x2122, 0x2122},
{0x2139, 0x2139}, {0x2194, 0x2199}, {0x21A9, 0x21AA},
{0x231A, 0x231B}, {0x2328, 0x2328}, {0x2388, 0x2388},
{0x23CF, 0x23CF}, {0x23E9, 0x23F3}, {0x23F8, 0x23FA},
{0x24C2, 0x24C2}, {0x25AA, 0x25AB}, {0x25B6, 0x25B6},
{0x25C0, 0x25C0}, {0x25FB, 0x25FE}, {0x2600, 0x2605},
{0x2607, 0x2612}, {0x2614, 0x2685}, {0x2690, 0x2705},
{0x2708, 0x2712}, {0x2714, 0x2714}, {0x2716, 0x2716},
{0x271D, 0x271D}, {0x2721, 0x2721}, {0x2728, 0x2728},
{0x2733, 0x2734}, {0x2744, 0x2744}, {0x2747, 0x2747},
{0x274C, 0x274C}, {0x274E, 0x274E}, {0x2753, 0x2755},
{0x2757, 0x2757}, {0x2763, 0x2767}, {0x2795, 0x2797},
{0x27A1, 0x27A1}, {0x27B0, 0x27B0}, {0x27BF, 0x27BF},
{0x2934, 0x2935}, {0x2B05, 0x2B07}, {0x2B1B, 0x2B1C},
{0x2B50, 0x2B50}, {0x2B55, 0x2B55}, {0x3030, 0x3030},
{0x303D, 0x303D}, {0x3297, 0x3297}, {0x3299, 0x3299},
{0x1F000, 0x1F0FF}, {0x1F10D, 0x1F10F}, {0x1F12F, 0x1F12F},
{0x1F16C, 0x1F171}, {0x1F17E, 0x1F17F}, {0x1F18E, 0x1F18E},
{0x1F191, 0x1F19A}, {0x1F1AD, 0x1F1E5}, {0x1F201, 0x1F20F},
{0x1F21A, 0x1F21A}, {0x1F22F, 0x1F22F}, {0x1F232, 0x1F23A},
{0x1F23C, 0x1F23F}, {0x1F249, 0x1F3FA}, {0x1F400, 0x1F53D},
{0x1F546, 0x1F64F}, {0x1F680, 0x1F6FF}, {0x1F774, 0x1F77F},
{0x1F7D5, 0x1F7FF}, {0x1F80C, 0x1F80F}, {0x1F848, 0x1F84F},
{0x1F85A, 0x1F85F}, {0x1F888, 0x1F88F}, {0x1F8AE, 0x1F8FF},
{0x1F90C, 0x1F93A}, {0x1F93C, 0x1F945}, {0x1F947, 0x1FFFD},
}

View File

@ -1,6 +1,3 @@
// +build windows
// +build !appengine
package runewidth
import (

21
vendor/github.com/nlopes/slack/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,21 @@
language: go
go:
- 1.7.x
- 1.8.x
- 1.9.x
- tip
before_install:
- export PATH=$HOME/gopath/bin:$PATH
script:
- go test -race ./...
- go test -cover ./...
matrix:
allow_failures:
- go: tip
git:
depth: 10

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

@ -0,0 +1,25 @@
### v0.3.0 - July 30, 2018
full differences can be viewed using `git log --oneline --decorate --color v0.2.0..v0.3.0`
- slack events initial support added. (still considered experimental and undergoing changes, stability not promised)
- vendored depedencies using dep, ensure using up to date tooling before filing issues.
- RTM has improved its ability to identify dead connections and reconnect automatically (worth calling out in case it has unintended side effects).
- bug fixes (various timestamp handling, error handling, RTM locking, etc).
### v0.2.0 - Feb 10, 2018
Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against.
Please check [0.2.0](https://github.com/nlopes/slack/releases/tag/v0.2.0)
### v0.1.0 - May 28, 2017
This is released before adding context support.
As the used context package is the one from Go 1.7 this will be the last
compatible with Go < 1.7.
Please check [0.1.0](https://github.com/nlopes/slack/releases/tag/v0.1.0)
### v0.0.1 - Jul 26, 2015
If you just updated from master and it broke your implementation, please
check [0.0.1](https://github.com/nlopes/slack/releases/tag/v0.0.1)

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

@ -0,0 +1,33 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
name = "github.com/gorilla/websocket"
packages = ["."]
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/stretchr/testify"
packages = ["assert"]
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "888307bf47ee004aaaa4c45e6139929b4984f2253e48e382246bfb8c66f3cd65"
solver-name = "gps-cdcl"
solver-version = 1

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

@ -0,0 +1,13 @@
ignored = ["github.com/lusis/slack-test"]
[[constraint]]
name = "github.com/gorilla/websocket"
version = "1.2.0"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"
[prune]
go-tests = true
unused-packages = true

View File

@ -1,6 +1,5 @@
Slack API in Go [![GoDoc](https://godoc.org/github.com/slack-go/slack?status.svg)](https://godoc.org/github.com/slack-go/slack) [![Build Status](https://travis-ci.org/slack-go/slack.svg)](https://travis-ci.org/slack-go/slack)
Slack API in Go [![GoDoc](https://godoc.org/github.com/nlopes/slack?status.svg)](https://godoc.org/github.com/nlopes/slack) [![Build Status](https://travis-ci.org/nlopes/slack.svg)](https://travis-ci.org/nlopes/slack)
===============
This is the original Slack library for Go created by Norberto Lopez, transferred to a Github organization.
[![Join the chat at https://gitter.im/go-slack/Lobby](https://badges.gitter.im/go-slack/Lobby.svg)](https://gitter.im/go-slack/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
@ -10,16 +9,24 @@ a fully managed way.
## Change log
Support for the EventsAPI has recently been added. It is still in its early stages but nearly all events have been added and tested (except for those events in [Developer Preview](https://api.slack.com/slack-apps-preview) mode). API stability for events is not promised at this time.
## Changelog
### v0.2.0 - Feb 10, 2018
[CHANGELOG.md](https://github.com/slack-go/slack/blob/master/CHANGELOG.md) is available. Please visit it for updates.
Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against.
Please check [0.2.0](https://github.com/nlopes/slack/releases/tag/v0.2.0)
### CHANGELOG.md
[CHANGELOG.md](https://github.com/nlopes/slack/blob/master/CHANGELOG.md) is available. Please visit it for updates.
## Installing
### *go get*
$ go get -u github.com/slack-go/slack
$ go get -u github.com/nlopes/slack
## Example
@ -29,14 +36,14 @@ a fully managed way.
import (
"fmt"
"github.com/slack-go/slack"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
// If you set debugging, it will log all requests to the console
// Useful when encountering issues
// slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true))
// api.SetDebug(true)
groups, err := api.GetGroups(false)
if err != nil {
fmt.Printf("%s\n", err)
@ -54,7 +61,7 @@ func main() {
import (
"fmt"
"github.com/slack-go/slack"
"github.com/nlopes/slack"
)
func main() {
@ -70,12 +77,12 @@ func main() {
## Minimal RTM usage:
See https://github.com/slack-go/slack/blob/master/examples/websocket/websocket.go
See https://github.com/nlopes/slack/blob/master/examples/websocket/websocket.go
## Minimal EventsAPI usage:
See https://github.com/slack-go/slack/blob/master/examples/eventsapi/events.go
See https://github.com/nlopes/slack/blob/master/examples/eventsapi/events.go
## Contributing
@ -83,14 +90,6 @@ See https://github.com/slack-go/slack/blob/master/examples/eventsapi/events.go
You are more than welcome to contribute to this project. Fork and
make a Pull Request, or create an Issue if you see any problem.
Before making any Pull Request please run the following:
```
make pr-prep
```
This will check/update code formatting, linting and then run all tests
## License
BSD 2 Clause license

View File

@ -2,19 +2,28 @@ package slack
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
)
func (api *Client) adminRequest(ctx context.Context, method string, teamName string, values url.Values) error {
resp := &SlackResponse{}
err := parseAdminResponse(ctx, api.httpclient, method, teamName, values, resp, api)
type adminResponse struct {
OK bool `json:"ok"`
Error string `json:"error"`
}
func adminRequest(ctx context.Context, client HTTPRequester, method string, teamName string, values url.Values, debug bool) (*adminResponse, error) {
adminResponse := &adminResponse{}
err := parseAdminResponse(ctx, client, method, teamName, values, adminResponse, debug)
if err != nil {
return err
return nil, err
}
return resp.Err()
if !adminResponse.OK {
return nil, errors.New(adminResponse.Error)
}
return adminResponse, nil
}
// DisableUser disabled a user account, given a user ID
@ -31,8 +40,9 @@ func (api *Client) DisableUserContext(ctx context.Context, teamName string, uid
"_attempts": {"1"},
}
if err := api.adminRequest(ctx, "setInactive", teamName, values); err != nil {
return fmt.Errorf("failed to disable user with id '%s': %s", uid, err)
_, err := adminRequest(ctx, api.httpclient, "setInactive", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err)
}
return nil
@ -57,7 +67,7 @@ func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, fi
"_attempts": {"1"},
}
err := api.adminRequest(ctx, "invite", teamName, values)
_, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to invite single-channel guest: %s", err)
}
@ -84,7 +94,7 @@ func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channe
"_attempts": {"1"},
}
err := api.adminRequest(ctx, "invite", teamName, values)
_, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to restricted account: %s", err)
}
@ -108,7 +118,7 @@ func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName,
"_attempts": {"1"},
}
err := api.adminRequest(ctx, "invite", teamName, values)
_, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to invite to team: %s", err)
}
@ -130,7 +140,7 @@ func (api *Client) SetRegularContext(ctx context.Context, teamName, user string)
"_attempts": {"1"},
}
err := api.adminRequest(ctx, "setRegular", teamName, values)
_, err := adminRequest(ctx, api.httpclient, "setRegular", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err)
}
@ -152,7 +162,7 @@ func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, use
"_attempts": {"1"},
}
err := api.adminRequest(ctx, "sendSSOBind", teamName, values)
_, err := adminRequest(ctx, api.httpclient, "sendSSOBind", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err)
}
@ -175,7 +185,7 @@ func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid,
"_attempts": {"1"},
}
err := api.adminRequest(ctx, "setUltraRestricted", teamName, values)
_, err := adminRequest(ctx, api.httpclient, "setUltraRestricted", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to ultra-restrict account: %s", err)
}
@ -184,23 +194,22 @@ func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid,
}
// SetRestricted converts a user into a restricted account
func (api *Client) SetRestricted(teamName, uid string, channelIds ...string) error {
return api.SetRestrictedContext(context.Background(), teamName, uid, channelIds...)
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, channelIds ...string) error {
func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid string) error {
values := url.Values{
"user": {uid},
"token": {api.token},
"set_active": {"true"},
"_attempts": {"1"},
"channels": {strings.Join(channelIds, ",")},
}
err := api.adminRequest(ctx, "setRestricted", teamName, values)
_, err := adminRequest(ctx, api.httpclient, "setRestricted", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("failed to restrict account: %s", err)
return fmt.Errorf("Failed to restrict account: %s", err)
}
return nil

View File

@ -17,7 +17,7 @@ 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 actionType `json:"type"` // Required. Must be set to "button" or "select".
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.
@ -28,11 +28,6 @@ type AttachmentAction struct {
URL string `json:"url,omitempty"` // Optional.
}
// actionType returns the type of the action
func (a AttachmentAction) actionType() actionType {
return a.Type
}
// AttachmentActionOption the individual option to appear in action menu.
type AttachmentActionOption struct {
Text string `json:"text"` // Required.
@ -47,8 +42,25 @@ type AttachmentActionOptionGroup struct {
}
// AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction)
// DEPRECATED: use InteractionCallback
type AttachmentActionCallback InteractionCallback
type AttachmentActionCallback struct {
Actions []AttachmentAction `json:"actions"`
CallbackID string `json:"callback_id"`
Team Team `json:"team"`
Channel Channel `json:"channel"`
User User `json:"user"`
Name string `json:"name"`
Value string `json:"value"`
OriginalMessage Message `json:"original_message"`
ActionTs string `json:"action_ts"`
MessageTs string `json:"message_ts"`
AttachmentID string `json:"attachment_id"`
Token string `json:"token"`
ResponseURL string `json:"response_url"`
TriggerID string `json:"trigger_id"`
}
// ConfirmationField are used to ask users to confirm actions
type ConfirmationField struct {
@ -61,7 +73,7 @@ type ConfirmationField struct {
// Attachment contains all the information for an attachment
type Attachment struct {
Color string `json:"color,omitempty"`
Fallback string `json:"fallback,omitempty"`
Fallback string `json:"fallback"`
CallbackID string `json:"callback_id,omitempty"`
ID int `json:"id,omitempty"`
@ -75,7 +87,7 @@ type Attachment struct {
Title string `json:"title,omitempty"`
TitleLink string `json:"title_link,omitempty"`
Pretext string `json:"pretext,omitempty"`
Text string `json:"text,omitempty"`
Text string `json:"text"`
ImageURL string `json:"image_url,omitempty"`
ThumbURL string `json:"thumb_url,omitempty"`
@ -84,8 +96,6 @@ type Attachment struct {
Actions []AttachmentAction `json:"actions,omitempty"`
MarkdownIn []string `json:"mrkdwn_in,omitempty"`
Blocks Blocks `json:"blocks,omitempty"`
Footer string `json:"footer,omitempty"`
FooterIcon string `json:"footer_icon,omitempty"`

57
vendor/github.com/nlopes/slack/backoff.go generated vendored Normal file
View File

@ -0,0 +1,57 @@
package slack
import (
"math"
"math/rand"
"time"
)
// This one was ripped from https://github.com/jpillora/backoff/blob/master/backoff.go
// Backoff is a time.Duration counter. It starts at Min. After every
// call to Duration() it is multiplied by Factor. It is capped at
// Max. It returns to Min on every call to Reset(). Used in
// conjunction with the time package.
type backoff struct {
attempts int
//Factor is the multiplying factor for each increment step
Factor float64
//Jitter eases contention by randomizing backoff steps
Jitter bool
//Min and Max are the minimum and maximum values of the counter
Min, Max time.Duration
}
// Returns the current value of the counter and then multiplies it
// Factor
func (b *backoff) Duration() time.Duration {
//Zero-values are nonsensical, so we use
//them to apply defaults
if b.Min == 0 {
b.Min = 100 * time.Millisecond
}
if b.Max == 0 {
b.Max = 10 * time.Second
}
if b.Factor == 0 {
b.Factor = 2
}
//calculate this duration
dur := float64(b.Min) * math.Pow(b.Factor, float64(b.attempts))
if b.Jitter {
dur = rand.Float64()*(dur-float64(b.Min)) + float64(b.Min)
}
//cap!
if dur > float64(b.Max) {
return b.Max
}
//bump attempts count
b.attempts++
//return as a time.Duration
return time.Duration(dur)
}
//Resets the current value of the counter back to Min
func (b *backoff) Reset() {
b.attempts = 0
}

View File

@ -2,18 +2,16 @@ package slack
import (
"context"
"errors"
"net/url"
)
// Bot contains information about a bot
type Bot struct {
ID string `json:"id"`
Name string `json:"name"`
Deleted bool `json:"deleted"`
UserID string `json:"user_id"`
AppID string `json:"app_id"`
Updated JSONTime `json:"updated"`
Icons Icons `json:"icons"`
ID string `json:"id"`
Name string `json:"name"`
Deleted bool `json:"deleted"`
Icons Icons `json:"icons"`
}
type botResponseFull struct {
@ -21,17 +19,15 @@ type botResponseFull struct {
SlackResponse
}
func (api *Client) botRequest(ctx context.Context, path string, values url.Values) (*botResponseFull, error) {
func botRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*botResponseFull, error) {
response := &botResponseFull{}
err := api.postMethod(ctx, path, values, response)
err := postSlackMethod(ctx, client, path, values, response, debug)
if err != nil {
return nil, err
}
if err := response.Err(); err != nil {
return nil, err
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
@ -44,13 +40,10 @@ func (api *Client) GetBotInfo(bot string) (*Bot, error) {
func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, error) {
values := url.Values{
"token": {api.token},
"bot": {bot},
}
if bot != "" {
values.Add("bot", bot)
}
response, err := api.botRequest(ctx, "bots.info", values)
response, err := botRequest(ctx, api.httpclient, "bots.info", values, api.debug)
if err != nil {
return nil, err
}

View File

@ -2,9 +2,9 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
"time"
)
type channelResponseFull struct {
@ -15,45 +15,27 @@ type channelResponseFull struct {
NotInChannel bool `json:"not_in_channel"`
History
SlackResponse
Metadata ResponseMetadata `json:"response_metadata"`
}
// Channel contains information about the channel
type Channel struct {
GroupConversation
groupConversation
IsChannel bool `json:"is_channel"`
IsGeneral bool `json:"is_general"`
IsMember bool `json:"is_member"`
Locale string `json:"locale"`
}
func (api *Client) channelRequest(ctx context.Context, path string, values url.Values) (*channelResponseFull, error) {
func channelRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*channelResponseFull, error) {
response := &channelResponseFull{}
err := postForm(ctx, api.httpclient, api.endpoint+path, values, response, api)
err := postForm(ctx, client, SLACK_API+path, values, response, debug)
if err != nil {
return nil, err
}
return response, response.Err()
}
// GetChannelsOption option provided when getting channels.
type GetChannelsOption func(*ChannelPagination) error
// GetChannelsOptionExcludeMembers excludes the members collection from each channel.
func GetChannelsOptionExcludeMembers() GetChannelsOption {
return func(p *ChannelPagination) error {
p.excludeMembers = true
return nil
}
}
// GetChannelsOptionExcludeArchived excludes archived channels from results.
func GetChannelsOptionExcludeArchived() GetChannelsOption {
return func(p *ChannelPagination) error {
p.excludeArchived = true
return nil
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// ArchiveChannel archives the given channel
@ -70,7 +52,7 @@ func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string)
"channel": {channelID},
}
_, err = api.channelRequest(ctx, "channels.archive", values)
_, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug)
return err
}
@ -88,7 +70,7 @@ func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string
"channel": {channelID},
}
_, err = api.channelRequest(ctx, "channels.unarchive", values)
_, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug)
return err
}
@ -106,7 +88,7 @@ func (api *Client) CreateChannelContext(ctx context.Context, channelName string)
"name": {channelName},
}
response, err := api.channelRequest(ctx, "channels.create", values)
response, err := channelRequest(ctx, api.httpclient, "channels.create", values, api.debug)
if err != nil {
return nil, err
}
@ -151,7 +133,7 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID strin
}
}
response, err := api.channelRequest(ctx, "channels.history", values)
response, err := channelRequest(ctx, api.httpclient, "channels.history", values, api.debug)
if err != nil {
return nil, err
}
@ -168,12 +150,11 @@ func (api *Client) GetChannelInfo(channelID string) (*Channel, error) {
// see https://api.slack.com/methods/channels.info
func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) (*Channel, error) {
values := url.Values{
"token": {api.token},
"channel": {channelID},
"include_locale": {strconv.FormatBool(true)},
"token": {api.token},
"channel": {channelID},
}
response, err := api.channelRequest(ctx, "channels.info", values)
response, err := channelRequest(ctx, api.httpclient, "channels.info", values, api.debug)
if err != nil {
return nil, err
}
@ -186,7 +167,7 @@ func (api *Client) InviteUserToChannel(channelID, user string) (*Channel, error)
return api.InviteUserToChannelContext(context.Background(), channelID, user)
}
// InviteUserToChannelContext invites a user to a given channel and returns a *Channel with a custom context
// InviteUserToChannelCustom invites a user to a given channel and returns a *Channel with a custom context
// see https://api.slack.com/methods/channels.invite
func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) {
values := url.Values{
@ -195,7 +176,7 @@ func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, us
"user": {user},
}
response, err := api.channelRequest(ctx, "channels.invite", values)
response, err := channelRequest(ctx, api.httpclient, "channels.invite", values, api.debug)
if err != nil {
return nil, err
}
@ -216,7 +197,7 @@ func (api *Client) JoinChannelContext(ctx context.Context, channelName string) (
"name": {channelName},
}
response, err := api.channelRequest(ctx, "channels.join", values)
response, err := channelRequest(ctx, api.httpclient, "channels.join", values, api.debug)
if err != nil {
return nil, err
}
@ -237,7 +218,7 @@ func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (b
"channel": {channelID},
}
response, err := api.channelRequest(ctx, "channels.leave", values)
response, err := channelRequest(ctx, api.httpclient, "channels.leave", values, api.debug)
if err != nil {
return false, err
}
@ -260,111 +241,31 @@ func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, us
"user": {user},
}
_, err = api.channelRequest(ctx, "channels.kick", values)
_, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug)
return err
}
func newChannelPagination(c *Client, options ...GetChannelsOption) (cp ChannelPagination) {
cp = ChannelPagination{
c: c,
limit: 200, // per slack api documentation.
}
for _, opt := range options {
opt(&cp)
}
return cp
}
// ChannelPagination allows for paginating over the channels
type ChannelPagination struct {
Channels []Channel
limit int
excludeArchived bool
excludeMembers bool
previousResp *ResponseMetadata
c *Client
}
// Done checks if the pagination has completed
func (ChannelPagination) Done(err error) bool {
return err == errPaginationComplete
}
// Failure checks if pagination failed.
func (t ChannelPagination) Failure(err error) error {
if t.Done(err) {
return nil
}
return err
}
func (t ChannelPagination) Next(ctx context.Context) (_ ChannelPagination, err error) {
var (
resp *channelResponseFull
)
if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") {
return t, errPaginationComplete
}
t.previousResp = t.previousResp.initialize()
values := url.Values{
"limit": {strconv.Itoa(t.limit)},
"exclude_archived": {strconv.FormatBool(t.excludeArchived)},
"exclude_members": {strconv.FormatBool(t.excludeMembers)},
"token": {t.c.token},
"cursor": {t.previousResp.Cursor},
}
if resp, err = t.c.channelRequest(ctx, "channels.list", values); err != nil {
return t, err
}
t.c.Debugf("GetChannelsContext: got %d channels; metadata %v", len(resp.Channels), resp.Metadata)
t.Channels = resp.Channels
t.previousResp = &resp.Metadata
return t, nil
}
// GetChannelsPaginated fetches channels in a paginated fashion, see GetChannelsContext for usage.
func (api *Client) GetChannelsPaginated(options ...GetChannelsOption) ChannelPagination {
return newChannelPagination(api, options...)
}
// GetChannels retrieves all the channels
// see https://api.slack.com/methods/channels.list
func (api *Client) GetChannels(excludeArchived bool, options ...GetChannelsOption) ([]Channel, error) {
return api.GetChannelsContext(context.Background(), excludeArchived, options...)
func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) {
return api.GetChannelsContext(context.Background(), excludeArchived)
}
// GetChannelsContext retrieves all the channels with a custom context
// see https://api.slack.com/methods/channels.list
func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool, options ...GetChannelsOption) (results []Channel, err error) {
func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool) ([]Channel, error) {
values := url.Values{
"token": {api.token},
}
if excludeArchived {
options = append(options, GetChannelsOptionExcludeArchived())
values.Add("exclude_archived", "1")
}
p := api.GetChannelsPaginated(options...)
for err == nil {
p, err = p.Next(ctx)
if err == nil {
results = append(results, p.Channels...)
} else if rateLimitedError, ok := err.(*RateLimitedError); ok {
select {
case <-ctx.Done():
err = ctx.Err()
case <-time.After(rateLimitedError.RetryAfter):
err = nil
}
}
response, err := channelRequest(ctx, api.httpclient, "channels.list", values, api.debug)
if err != nil {
return nil, err
}
return results, p.Failure(err)
return response.Channels, nil
}
// SetChannelReadMark sets the read mark of a given channel to a specific point
@ -387,7 +288,7 @@ func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts
"ts": {ts},
}
_, err = api.channelRequest(ctx, "channels.mark", values)
_, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug)
return err
}
@ -408,7 +309,7 @@ func (api *Client) RenameChannelContext(ctx context.Context, channelID, name str
// 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 := api.channelRequest(ctx, "channels.rename", values)
response, err := channelRequest(ctx, api.httpclient, "channels.rename", values, api.debug)
if err != nil {
return nil, err
}
@ -430,7 +331,7 @@ func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purp
"purpose": {purpose},
}
response, err := api.channelRequest(ctx, "channels.setPurpose", values)
response, err := channelRequest(ctx, api.httpclient, "channels.setPurpose", values, api.debug)
if err != nil {
return "", err
}
@ -452,7 +353,7 @@ func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic
"topic": {topic},
}
response, err := api.channelRequest(ctx, "channels.setTopic", values)
response, err := channelRequest(ctx, api.httpclient, "channels.setTopic", values, api.debug)
if err != nil {
return "", err
}
@ -473,7 +374,7 @@ func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thre
"channel": {channelID},
"thread_ts": {thread_ts},
}
response, err := api.channelRequest(ctx, "channels.replies", values)
response, err := channelRequest(ctx, api.httpclient, "channels.replies", values, api.debug)
if err != nil {
return nil, err
}

448
vendor/github.com/nlopes/slack/chat.go generated vendored Normal file
View File

@ -0,0 +1,448 @@
package slack
import (
"context"
"encoding/json"
"net/url"
"strings"
)
const (
DEFAULT_MESSAGE_USERNAME = ""
DEFAULT_MESSAGE_REPLY_BROADCAST = false
DEFAULT_MESSAGE_ASUSER = false
DEFAULT_MESSAGE_PARSE = ""
DEFAULT_MESSAGE_THREAD_TIMESTAMP = ""
DEFAULT_MESSAGE_LINK_NAMES = 0
DEFAULT_MESSAGE_UNFURL_LINKS = false
DEFAULT_MESSAGE_UNFURL_MEDIA = true
DEFAULT_MESSAGE_ICON_URL = ""
DEFAULT_MESSAGE_ICON_EMOJI = ""
DEFAULT_MESSAGE_MARKDOWN = true
DEFAULT_MESSAGE_ESCAPE_TEXT = true
)
type chatResponseFull struct {
Channel string `json:"channel"`
Timestamp string `json:"ts"` //Regualr message timestamp
MessageTimeStamp string `json:"message_ts"` //Ephemeral message timestamp
Text string `json:"text"`
SlackResponse
}
// getMessageTimestamp will inspect the `chatResponseFull` to ruturn a timestamp value
// in `chat.postMessage` its under `ts`
// in `chat.postEphemeral` its under `message_ts`
func (c chatResponseFull) getMessageTimestamp() string {
if len(c.Timestamp) > 0 {
return c.Timestamp
}
return c.MessageTimeStamp
}
// PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request
type PostMessageParameters struct {
Username string `json:"username"`
AsUser bool `json:"as_user"`
Parse string `json:"parse"`
ThreadTimestamp string `json:"thread_ts"`
ReplyBroadcast bool `json:"reply_broadcast"`
LinkNames int `json:"link_names"`
Attachments []Attachment `json:"attachments"`
UnfurlLinks bool `json:"unfurl_links"`
UnfurlMedia bool `json:"unfurl_media"`
IconURL string `json:"icon_url"`
IconEmoji string `json:"icon_emoji"`
Markdown bool `json:"mrkdwn,omitempty"`
EscapeText bool `json:"escape_text"`
// chat.postEphemeral support
Channel string `json:"channel"`
User string `json:"user"`
}
// NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set
func NewPostMessageParameters() PostMessageParameters {
return PostMessageParameters{
Username: DEFAULT_MESSAGE_USERNAME,
User: DEFAULT_MESSAGE_USERNAME,
AsUser: DEFAULT_MESSAGE_ASUSER,
Parse: DEFAULT_MESSAGE_PARSE,
ThreadTimestamp: DEFAULT_MESSAGE_THREAD_TIMESTAMP,
LinkNames: DEFAULT_MESSAGE_LINK_NAMES,
Attachments: nil,
UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS,
UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA,
IconURL: DEFAULT_MESSAGE_ICON_URL,
IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI,
Markdown: DEFAULT_MESSAGE_MARKDOWN,
EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT,
}
}
// DeleteMessage deletes a message in a channel
func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) {
respChannel, respTimestamp, _, err := api.SendMessageContext(context.Background(), channel, MsgOptionDelete(messageTimestamp))
return respChannel, respTimestamp, err
}
// DeleteMessageContext deletes a message in a channel with a custom context
func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTimestamp string) (string, string, error) {
respChannel, respTimestamp, _, err := api.SendMessageContext(ctx, channel, MsgOptionDelete(messageTimestamp))
return respChannel, respTimestamp, err
}
// PostMessage sends a message to a channel.
// Message is escaped by default according to https://api.slack.com/docs/formatting
// Use http://davestevens.github.io/slack-message-builder/ to help crafting your message.
func (api *Client) PostMessage(channel, text string, params PostMessageParameters) (string, string, error) {
respChannel, respTimestamp, _, err := api.SendMessageContext(
context.Background(),
channel,
MsgOptionText(text, params.EscapeText),
MsgOptionAttachments(params.Attachments...),
MsgOptionPostMessageParameters(params),
)
return respChannel, respTimestamp, err
}
// PostMessageContext sends a message to a channel with a custom context
// For more details, see PostMessage documentation
func (api *Client) PostMessageContext(ctx context.Context, channel, text string, params PostMessageParameters) (string, string, error) {
respChannel, respTimestamp, _, err := api.SendMessageContext(
ctx,
channel,
MsgOptionText(text, params.EscapeText),
MsgOptionAttachments(params.Attachments...),
MsgOptionPostMessageParameters(params),
)
return respChannel, respTimestamp, err
}
// PostEphemeral sends an ephemeral message to a user in a channel.
// Message is escaped by default according to https://api.slack.com/docs/formatting
// Use http://davestevens.github.io/slack-message-builder/ to help crafting your message.
func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption) (string, error) {
return api.PostEphemeralContext(
context.Background(),
channelID,
userID,
options...,
)
}
// PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context
// For more details, see PostEphemeral documentation
func (api *Client) PostEphemeralContext(ctx context.Context, channelID, userID string, options ...MsgOption) (timestamp string, err error) {
_, timestamp, _, err = api.SendMessageContext(ctx, channelID, append(options, MsgOptionPostEphemeral2(userID))...)
return timestamp, err
}
// UpdateMessage updates a message in a channel
func (api *Client) UpdateMessage(channelID, timestamp, text string) (string, string, string, error) {
return api.UpdateMessageContext(context.Background(), channelID, timestamp, text)
}
// UpdateMessageContext updates a message in a channel
func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp, text string) (string, string, string, error) {
return api.SendMessageContext(ctx, channelID, MsgOptionUpdate(timestamp), MsgOptionText(text, true))
}
// SendMessage more flexible method for configuring messages.
func (api *Client) SendMessage(channel string, options ...MsgOption) (string, string, string, error) {
return api.SendMessageContext(context.Background(), channel, options...)
}
// SendMessageContext more flexible method for configuring messages with a custom context.
func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (channel string, timestamp string, text string, err error) {
var (
config sendConfig
response chatResponseFull
)
if config, err = applyMsgOptions(api.token, channelID, options...); err != nil {
return "", "", "", err
}
if err = postSlackMethod(ctx, api.httpclient, string(config.mode), config.values, &response, api.debug); err != nil {
return "", "", "", err
}
return response.Channel, response.getMessageTimestamp(), response.Text, response.Err()
}
// ApplyMsgOptions utility function for debugging/testing chat requests.
func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) {
config, err := applyMsgOptions(token, channel, options...)
return string(config.mode), config.values, err
}
func applyMsgOptions(token, channel string, options ...MsgOption) (sendConfig, error) {
config := sendConfig{
mode: chatPostMessage,
values: url.Values{
"token": {token},
"channel": {channel},
},
}
for _, opt := range options {
if err := opt(&config); err != nil {
return config, err
}
}
return config, nil
}
func escapeMessage(message string) string {
replacer := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
return replacer.Replace(message)
}
type sendMode string
const (
chatUpdate sendMode = "chat.update"
chatPostMessage sendMode = "chat.postMessage"
chatDelete sendMode = "chat.delete"
chatPostEphemeral sendMode = "chat.postEphemeral"
chatMeMessage sendMode = "chat.meMessage"
)
type sendConfig struct {
mode sendMode
values url.Values
}
// MsgOption option provided when sending a message.
type MsgOption func(*sendConfig) error
// MsgOptionPost posts a messages, this is the default.
func MsgOptionPost() MsgOption {
return func(config *sendConfig) error {
config.mode = chatPostMessage
config.values.Del("ts")
return nil
}
}
// MsgOptionPostEphemeral - DEPRECATED: use MsgOptionPostEphemeral2
// posts an ephemeral message.
func MsgOptionPostEphemeral() MsgOption {
return func(config *sendConfig) error {
config.mode = chatPostEphemeral
config.values.Del("ts")
return nil
}
}
// MsgOptionPostEphemeral2 - posts an ephemeral message to the provided user.
func MsgOptionPostEphemeral2(userID string) MsgOption {
return func(config *sendConfig) error {
config.mode = chatPostEphemeral
MsgOptionUser(userID)(config)
config.values.Del("ts")
return nil
}
}
// MsgOptionMeMessage posts a "me message" type from the calling user
func MsgOptionMeMessage() MsgOption {
return func(config *sendConfig) error {
config.mode = chatMeMessage
return nil
}
}
// MsgOptionUpdate updates a message based on the timestamp.
func MsgOptionUpdate(timestamp string) MsgOption {
return func(config *sendConfig) error {
config.mode = chatUpdate
config.values.Add("ts", timestamp)
return nil
}
}
// MsgOptionDelete deletes a message based on the timestamp.
func MsgOptionDelete(timestamp string) MsgOption {
return func(config *sendConfig) error {
config.mode = chatDelete
config.values.Add("ts", timestamp)
return nil
}
}
// MsgOptionAsUser whether or not to send the message as the user.
func MsgOptionAsUser(b bool) MsgOption {
return func(config *sendConfig) error {
if b != DEFAULT_MESSAGE_ASUSER {
config.values.Set("as_user", "true")
}
return nil
}
}
// MsgOptionUser set the user for the message.
func MsgOptionUser(userID string) MsgOption {
return func(config *sendConfig) error {
config.values.Set("user", userID)
return nil
}
}
// MsgOptionText provide the text for the message, optionally escape the provided
// text.
func MsgOptionText(text string, escape bool) MsgOption {
return func(config *sendConfig) error {
if escape {
text = escapeMessage(text)
}
config.values.Add("text", text)
return nil
}
}
// MsgOptionAttachments provide attachments for the message.
func MsgOptionAttachments(attachments ...Attachment) MsgOption {
return func(config *sendConfig) error {
if attachments == nil {
return nil
}
attachments, err := json.Marshal(attachments)
if err == nil {
config.values.Set("attachments", string(attachments))
}
return err
}
}
// MsgOptionEnableLinkUnfurl enables link unfurling
func MsgOptionEnableLinkUnfurl() MsgOption {
return func(config *sendConfig) error {
config.values.Set("unfurl_links", "true")
return nil
}
}
// MsgOptionDisableLinkUnfurl disables link unfurling
func MsgOptionDisableLinkUnfurl() MsgOption {
return func(config *sendConfig) error {
config.values.Set("unfurl_links", "false")
return nil
}
}
// MsgOptionDisableMediaUnfurl disables media unfurling.
func MsgOptionDisableMediaUnfurl() MsgOption {
return func(config *sendConfig) error {
config.values.Set("unfurl_media", "false")
return nil
}
}
// MsgOptionDisableMarkdown disables markdown.
func MsgOptionDisableMarkdown() MsgOption {
return func(config *sendConfig) error {
config.values.Set("mrkdwn", "false")
return nil
}
}
// MsgOptionTS sets the thread TS of the message to enable creating or replying to a thread
func MsgOptionTS(ts string) MsgOption {
return func(config *sendConfig) error {
config.values.Set("thread_ts", ts)
return nil
}
}
// MsgOptionBroadcast sets reply_broadcast to true
func MsgOptionBroadcast() MsgOption {
return func(config *sendConfig) error {
config.values.Set("reply_broadcast", "true")
return nil
}
}
// this function combines multiple options into a single option.
func MsgOptionCompose(options ...MsgOption) MsgOption {
return func(c *sendConfig) error {
for _, opt := range options {
if err := opt(c); err != nil {
return err
}
}
return nil
}
}
func MsgOptionParse(b bool) MsgOption {
return func(c *sendConfig) error {
var v string
if b {
v = "1"
} else {
v = "0"
}
c.values.Set("parse", v)
return nil
}
}
// MsgOptionPostMessageParameters maintain backwards compatibility.
func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
return func(config *sendConfig) error {
if params.Username != DEFAULT_MESSAGE_USERNAME {
config.values.Set("username", params.Username)
}
// chat.postEphemeral support
if params.User != DEFAULT_MESSAGE_USERNAME {
config.values.Set("user", params.User)
}
// never generates an error.
MsgOptionAsUser(params.AsUser)(config)
if params.Parse != DEFAULT_MESSAGE_PARSE {
config.values.Set("parse", params.Parse)
}
if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES {
config.values.Set("link_names", "1")
}
if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS {
config.values.Set("unfurl_links", "true")
}
// I want to send a message with explicit `as_user` `true` and `unfurl_links` `false` in request.
// Because setting `as_user` to `true` will change the default value for `unfurl_links` to `true` on Slack API side.
if params.AsUser != DEFAULT_MESSAGE_ASUSER && params.UnfurlLinks == DEFAULT_MESSAGE_UNFURL_LINKS {
config.values.Set("unfurl_links", "false")
}
if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA {
config.values.Set("unfurl_media", "false")
}
if params.IconURL != DEFAULT_MESSAGE_ICON_URL {
config.values.Set("icon_url", params.IconURL)
}
if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI {
config.values.Set("icon_emoji", params.IconEmoji)
}
if params.Markdown != DEFAULT_MESSAGE_MARKDOWN {
config.values.Set("mrkdwn", "false")
}
if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP {
config.values.Set("thread_ts", params.ThreadTimestamp)
}
if params.ReplyBroadcast != DEFAULT_MESSAGE_REPLY_BROADCAST {
config.values.Set("reply_broadcast", "true")
}
return nil
}
}

View File

@ -2,13 +2,14 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
"strings"
)
// Conversation is the foundation for IM and BaseGroupConversation
type Conversation struct {
type conversation struct {
ID string `json:"id"`
Created JSONTime `json:"created"`
IsOpen bool `json:"is_open"`
@ -35,8 +36,8 @@ type Conversation struct {
}
// GroupConversation is the foundation for Group and Channel
type GroupConversation struct {
Conversation
type groupConversation struct {
conversation
Name string `json:"name"`
Creator string `json:"creator"`
IsArchived bool `json:"is_archived"`
@ -65,14 +66,6 @@ type GetUsersInConversationParameters struct {
Limit int
}
type GetConversationsForUserParameters struct {
UserID string
Cursor string
Types []string
Limit int
ExcludeArchived bool
}
type responseMetaData struct {
NextCursor string `json:"next_cursor"`
}
@ -99,57 +92,16 @@ func (api *Client) GetUsersInConversationContext(ctx context.Context, params *Ge
ResponseMetaData responseMetaData `json:"response_metadata"`
SlackResponse
}{}
err := api.postMethod(ctx, "conversations.members", values, &response)
err := postSlackMethod(ctx, api.httpclient, "conversations.members", values, &response, api.debug)
if err != nil {
return nil, "", err
}
if err := response.Err(); err != nil {
return nil, "", err
if !response.Ok {
return nil, "", errors.New(response.Error)
}
return response.Members, response.ResponseMetaData.NextCursor, nil
}
// GetConversationsForUser returns the list conversations for a given user
func (api *Client) GetConversationsForUser(params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) {
return api.GetConversationsForUserContext(context.Background(), params)
}
// GetConversationsForUserContext returns the list conversations for a given user with a custom context
func (api *Client) GetConversationsForUserContext(ctx context.Context, params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) {
values := url.Values{
"token": {api.token},
}
if params.UserID != "" {
values.Add("user", params.UserID)
}
if params.Cursor != "" {
values.Add("cursor", params.Cursor)
}
if params.Limit != 0 {
values.Add("limit", strconv.Itoa(params.Limit))
}
if params.Types != nil {
values.Add("types", strings.Join(params.Types, ","))
}
if params.ExcludeArchived {
values.Add("exclude_archived", "true")
}
response := struct {
Channels []Channel `json:"channels"`
ResponseMetaData responseMetaData `json:"response_metadata"`
SlackResponse
}{}
err = api.postMethod(ctx, "users.conversations", values, &response)
if err != nil {
return nil, "", err
}
return response.Channels, response.ResponseMetaData.NextCursor, response.Err()
}
// ArchiveConversation archives a conversation
func (api *Client) ArchiveConversation(channelID string) error {
return api.ArchiveConversationContext(context.Background(), channelID)
@ -161,9 +113,8 @@ func (api *Client) ArchiveConversationContext(ctx context.Context, channelID str
"token": {api.token},
"channel": {channelID},
}
response := SlackResponse{}
err := api.postMethod(ctx, "conversations.archive", values, &response)
err := postSlackMethod(ctx, api.httpclient, "conversations.archive", values, &response, api.debug)
if err != nil {
return err
}
@ -183,7 +134,7 @@ func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID s
"channel": {channelID},
}
response := SlackResponse{}
err := api.postMethod(ctx, "conversations.unarchive", values, &response)
err := postSlackMethod(ctx, api.httpclient, "conversations.unarchive", values, &response, api.debug)
if err != nil {
return err
}
@ -207,7 +158,7 @@ func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID,
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := api.postMethod(ctx, "conversations.setTopic", values, &response)
err := postSlackMethod(ctx, api.httpclient, "conversations.setTopic", values, &response, api.debug)
if err != nil {
return nil, err
}
@ -231,8 +182,7 @@ func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelI
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := api.postMethod(ctx, "conversations.setPurpose", values, &response)
err := postSlackMethod(ctx, api.httpclient, "conversations.setPurpose", values, &response, api.debug)
if err != nil {
return nil, err
}
@ -256,8 +206,7 @@ func (api *Client) RenameConversationContext(ctx context.Context, channelID, cha
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := api.postMethod(ctx, "conversations.rename", values, &response)
err := postSlackMethod(ctx, api.httpclient, "conversations.rename", values, &response, api.debug)
if err != nil {
return nil, err
}
@ -281,8 +230,7 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := api.postMethod(ctx, "conversations.invite", values, &response)
err := postSlackMethod(ctx, api.httpclient, "conversations.invite", values, &response, api.debug)
if err != nil {
return nil, err
}
@ -302,9 +250,8 @@ func (api *Client) KickUserFromConversationContext(ctx context.Context, channelI
"channel": {channelID},
"user": {user},
}
response := SlackResponse{}
err := api.postMethod(ctx, "conversations.kick", values, &response)
err := postSlackMethod(ctx, api.httpclient, "conversations.kick", values, &response, api.debug)
if err != nil {
return err
}
@ -329,7 +276,7 @@ func (api *Client) CloseConversationContext(ctx context.Context, channelID strin
AlreadyClosed bool `json:"already_closed"`
}{}
err = api.postMethod(ctx, "conversations.close", values, &response)
err = postSlackMethod(ctx, api.httpclient, "conversations.close", values, &response, api.debug)
if err != nil {
return false, false, err
}
@ -349,12 +296,13 @@ func (api *Client) CreateConversationContext(ctx context.Context, channelName st
"name": {channelName},
"is_private": {strconv.FormatBool(isPrivate)},
}
response, err := api.channelRequest(ctx, "conversations.create", values)
response, err := channelRequest(
ctx, api.httpclient, "conversations.create", values, api.debug)
if err != nil {
return nil, err
}
return &response.Channel, nil
return &response.Channel, response.Err()
}
// GetConversationInfo retrieves information about a conversation
@ -369,7 +317,8 @@ func (api *Client) GetConversationInfoContext(ctx context.Context, channelID str
"channel": {channelID},
"include_locale": {strconv.FormatBool(includeLocale)},
}
response, err := api.channelRequest(ctx, "conversations.info", values)
response, err := channelRequest(
ctx, api.httpclient, "conversations.info", values, api.debug)
if err != nil {
return nil, err
}
@ -389,7 +338,7 @@ func (api *Client) LeaveConversationContext(ctx context.Context, channelID strin
"channel": {channelID},
}
response, err := api.channelRequest(ctx, "conversations.leave", values)
response, err := channelRequest(ctx, api.httpclient, "conversations.leave", values, api.debug)
if err != nil {
return false, err
}
@ -445,7 +394,7 @@ func (api *Client) GetConversationRepliesContext(ctx context.Context, params *Ge
Messages []Message `json:"messages"`
}{}
err = api.postMethod(ctx, "conversations.replies", values, &response)
err = postSlackMethod(ctx, api.httpclient, "conversations.replies", values, &response, api.debug)
if err != nil {
return nil, false, "", err
}
@ -485,8 +434,7 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve
ResponseMetaData responseMetaData `json:"response_metadata"`
SlackResponse
}{}
err = api.postMethod(ctx, "conversations.list", values, &response)
err = postSlackMethod(ctx, api.httpclient, "conversations.list", values, &response, api.debug)
if err != nil {
return nil, "", err
}
@ -523,8 +471,7 @@ func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConv
AlreadyOpen bool `json:"already_open"`
SlackResponse
}{}
err := api.postMethod(ctx, "conversations.open", values, &response)
err := postSlackMethod(ctx, api.httpclient, "conversations.open", values, &response, api.debug)
if err != nil {
return nil, false, false, err
}
@ -548,8 +495,7 @@ func (api *Client) JoinConversationContext(ctx context.Context, channelID string
} `json:"response_metadata"`
SlackResponse
}{}
err := api.postMethod(ctx, "conversations.join", values, &response)
err := postSlackMethod(ctx, api.httpclient, "conversations.join", values, &response, api.debug)
if err != nil {
return nil, "", nil, err
}
@ -611,10 +557,12 @@ func (api *Client) GetConversationHistoryContext(ctx context.Context, params *Ge
response := GetConversationHistoryResponse{}
err := api.postMethod(ctx, "conversations.history", values, &response)
err := postSlackMethod(ctx, api.httpclient, "conversations.history", values, &response, api.debug)
if err != nil {
return nil, err
}
return &response, response.Err()
if !response.Ok {
return nil, errors.New(response.Error)
}
return &response, nil
}

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

@ -0,0 +1,107 @@
package slack
import (
"context"
"encoding/json"
"errors"
)
type DialogTrigger struct {
TriggerId string `json:"trigger_id"` //Required. Must respond within 3 seconds.
Dialog Dialog `json:"dialog"` //Required.
}
type Dialog struct {
CallbackId string `json:"callback_id"` //Required.
Title string `json:"title"` //Required.
SubmitLabel string `json:"submit_label,omitempty"` //Optional. Default value is 'Submit'
NotifyOnCancel bool `json:"notify_on_cancel,omitempty"` //Optional. Default value is false
Elements []DialogElement `json:"elements"` //Required.
}
type DialogElement interface{}
type DialogTextElement struct {
Label string `json:"label"` //Required.
Name string `json:"name"` //Required.
Type string `json:"type"` //Required. Allowed values: "text", "textarea", "select".
Placeholder string `json:"placeholder,omitempty"` //Optional.
Optional bool `json:"optional,omitempty"` //Optional. Default value is false
Value string `json:"value,omitempty"` //Optional.
MaxLength int `json:"max_length,omitempty"` //Optional.
MinLength int `json:"min_length,omitempty"` //Optional,. Default value is 0
Hint string `json:"hint,omitempty"` //Optional.
Subtype string `json:"subtype,omitempty"` //Optional. Allowed values: "email", "number", "tel", "url".
}
type DialogSelectElement struct {
Label string `json:"label"` //Required.
Name string `json:"name"` //Required.
Type string `json:"type"` //Required. Allowed values: "text", "textarea", "select".
Placeholder string `json:"placeholder,omitempty"` //Optional.
Optional bool `json:"optional,omitempty"` //Optional. Default value is false
Value string `json:"value,omitempty"` //Optional.
DataSource string `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external".
SelectedOptions string `json:"selected_options,omitempty"` //Optional. Default value for "external" only
Options []DialogElementOption `json:"options,omitempty"` //One of options or option_groups is required.
OptionGroups []DialogElementOption `json:"option_groups,omitempty"` //Provide up to 100 options.
}
type DialogElementOption struct {
Label string `json:"label"` //Required.
Value string `json:"value"` //Required.
}
// DialogCallback is sent from Slack when a user submits a form from within a dialog
type DialogCallback struct {
Type string `json:"type"`
CallbackID string `json:"callback_id"`
Team Team `json:"team"`
Channel Channel `json:"channel"`
User User `json:"user"`
ActionTs string `json:"action_ts"`
Token string `json:"token"`
ResponseURL string `json:"response_url"`
Submission map[string]string `json:"submission"`
}
// DialogSuggestionCallback is sent from Slack when a user types in a select field with an external data source
type DialogSuggestionCallback struct {
Type string `json:"type"`
Token string `json:"token"`
ActionTs string `json:"action_ts"`
Team Team `json:"team"`
User User `json:"user"`
Channel Channel `json:"channel"`
ElementName string `json:"name"`
Value string `json:"value"`
CallbackID string `json:"callback_id"`
}
// OpenDialog opens a dialog window where the triggerId originated from
func (api *Client) OpenDialog(triggerId string, dialog Dialog) (err error) {
return api.OpenDialogContext(context.Background(), triggerId, dialog)
}
// OpenDialogContext opens a dialog window where the triggerId originated from with a custom context
func (api *Client) OpenDialogContext(ctx context.Context, triggerId string, dialog Dialog) (err error) {
if triggerId == "" {
return errors.New("received empty parameters")
}
resp := DialogTrigger{
TriggerId: triggerId,
Dialog: dialog,
}
jsonResp, err := json.Marshal(resp)
if err != nil {
return err
}
response := &SlackResponse{}
endpoint := SLACK_API + "dialog.open"
if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonResp, response, api.debug); err != nil {
return err
}
return response.Err()
}

View File

@ -2,6 +2,7 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
"strings"
@ -35,14 +36,16 @@ type dndTeamInfoResponse struct {
SlackResponse
}
func (api *Client) dndRequest(ctx context.Context, path string, values url.Values) (*dndResponseFull, error) {
func dndRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*dndResponseFull, error) {
response := &dndResponseFull{}
err := api.postMethod(ctx, path, values, response)
err := postSlackMethod(ctx, client, path, values, response, debug)
if err != nil {
return nil, err
}
return response, response.Err()
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// EndDND ends the user's scheduled Do Not Disturb session
@ -58,7 +61,7 @@ func (api *Client) EndDNDContext(ctx context.Context) error {
response := &SlackResponse{}
if err := api.postMethod(ctx, "dnd.endDnd", values, response); err != nil {
if err := postSlackMethod(ctx, api.httpclient, "dnd.endDnd", values, response, api.debug); err != nil {
return err
}
@ -76,7 +79,7 @@ func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) {
"token": {api.token},
}
response, err := api.dndRequest(ctx, "dnd.endSnooze", values)
response, err := dndRequest(ctx, api.httpclient, "dnd.endSnooze", values, api.debug)
if err != nil {
return nil, err
}
@ -97,7 +100,7 @@ func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDSta
values.Set("user", *user)
}
response, err := api.dndRequest(ctx, "dnd.info", values)
response, err := dndRequest(ctx, api.httpclient, "dnd.info", values, api.debug)
if err != nil {
return nil, err
}
@ -117,14 +120,12 @@ func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (m
}
response := &dndTeamInfoResponse{}
if err := api.postMethod(ctx, "dnd.teamInfo", values, response); err != nil {
if err := postSlackMethod(ctx, api.httpclient, "dnd.teamInfo", values, response, api.debug); err != nil {
return nil, err
}
if response.Err() != nil {
return nil, response.Err()
if !response.Ok {
return nil, errors.New(response.Error)
}
return response.Users, nil
}
@ -135,7 +136,7 @@ func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) {
return api.SetSnoozeContext(context.Background(), minutes)
}
// SetSnoozeContext adjusts the snooze duration for a user's Do Not Disturb settings with a custom context.
// 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{
@ -143,7 +144,7 @@ func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatu
"num_minutes": {strconv.Itoa(minutes)},
}
response, err := api.dndRequest(ctx, "dnd.setSnooze", values)
response, err := dndRequest(ctx, api.httpclient, "dnd.setSnooze", values, api.debug)
if err != nil {
return nil, err
}

View File

@ -2,6 +2,7 @@ package slack
import (
"context"
"errors"
"net/url"
)
@ -22,14 +23,12 @@ func (api *Client) GetEmojiContext(ctx context.Context) (map[string]string, erro
}
response := &emojiResponseFull{}
err := api.postMethod(ctx, "emoji.list", values, response)
err := postSlackMethod(ctx, api.httpclient, "emoji.list", values, response, api.debug)
if err != nil {
return nil, err
}
if response.Err() != nil {
return nil, response.Err()
if !response.Ok {
return nil, errors.New(response.Error)
}
return response.Emoji, nil
}

View File

@ -2,7 +2,7 @@ package slack
import (
"context"
"fmt"
"errors"
"io"
"net/url"
"strconv"
@ -86,41 +86,21 @@ type File struct {
CommentsCount int `json:"comments_count"`
NumStars int `json:"num_stars"`
IsStarred bool `json:"is_starred"`
Shares Share `json:"shares"`
}
type Share struct {
Public map[string][]ShareFileInfo `json:"public"`
Private map[string][]ShareFileInfo `json:"private"`
}
type ShareFileInfo struct {
ReplyUsers []string `json:"reply_users"`
ReplyUsersCount int `json:"reply_users_count"`
ReplyCount int `json:"reply_count"`
Ts string `json:"ts"`
ThreadTs string `json:"thread_ts"`
LatestReply string `json:"latest_reply"`
ChannelName string `json:"channel_name"`
TeamID string `json:"team_id"`
}
// 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.
//
// Note that when using the Reader option, you *must* specify the Filename, otherwise the Slack API isn't happy.
type FileUploadParameters struct {
File string
Content string
Reader io.Reader
Filetype string
Filename string
Title string
InitialComment string
Channels []string
ThreadTimestamp string
File string
Content string
Reader io.Reader
Filetype string
Filename string
Title string
InitialComment string
Channels []string
}
// GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request
@ -134,21 +114,11 @@ type GetFilesParameters struct {
Page int
}
// ListFilesParameters contains all the parameters necessary (including the optional ones) for a ListFiles() request
type ListFilesParameters struct {
Limit int
User string
Channel string
Types string
Cursor string
}
type fileResponseFull struct {
File `json:"file"`
Paging `json:"paging"`
Comments []Comment `json:"comments"`
Files []File `json:"files"`
Metadata ResponseMetadata `json:"response_metadata"`
Comments []Comment `json:"comments"`
Files []File `json:"files"`
SlackResponse
}
@ -166,14 +136,16 @@ func NewGetFilesParameters() GetFilesParameters {
}
}
func (api *Client) fileRequest(ctx context.Context, path string, values url.Values) (*fileResponseFull, error) {
func fileRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*fileResponseFull, error) {
response := &fileResponseFull{}
err := api.postMethod(ctx, path, values, response)
err := postForm(ctx, client, SLACK_API+path, values, response, debug)
if err != nil {
return nil, err
}
return response, response.Err()
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// GetFileInfo retrieves a file and related comments
@ -190,57 +162,18 @@ func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count,
"page": {strconv.Itoa(page)},
}
response, err := api.fileRequest(ctx, "files.info", values)
response, err := fileRequest(ctx, api.httpclient, "files.info", values, api.debug)
if err != nil {
return nil, nil, nil, err
}
return &response.File, response.Comments, &response.Paging, nil
}
// GetFile retreives a given file from its private download URL
func (api *Client) GetFile(downloadURL string, writer io.Writer) error {
return downloadFile(api.httpclient, api.token, downloadURL, writer, api)
}
// GetFiles retrieves all files according to the parameters given
func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) {
return api.GetFilesContext(context.Background(), params)
}
// ListFiles retrieves all files according to the parameters given. Uses cursor based pagination.
func (api *Client) ListFiles(params ListFilesParameters) ([]File, *ListFilesParameters, error) {
return api.ListFilesContext(context.Background(), params)
}
// ListFilesContext retrieves all files according to the parameters given with a custom context. Uses cursor based pagination.
func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParameters) ([]File, *ListFilesParameters, error) {
values := url.Values{
"token": {api.token},
}
if params.User != DEFAULT_FILES_USER {
values.Add("user", params.User)
}
if params.Channel != DEFAULT_FILES_CHANNEL {
values.Add("channel", params.Channel)
}
if params.Limit != DEFAULT_FILES_COUNT {
values.Add("limit", strconv.Itoa(params.Limit))
}
if params.Cursor != "" {
values.Add("cursor", params.Cursor)
}
response, err := api.fileRequest(ctx, "files.list", values)
if err != nil {
return nil, nil, err
}
params.Cursor = response.Metadata.Cursor
return response.Files, &params, nil
}
// 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{
@ -268,7 +201,7 @@ func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameter
values.Add("page", strconv.Itoa(params.Page))
}
response, err := api.fileRequest(ctx, "files.list", values)
response, err := fileRequest(ctx, api.httpclient, "files.list", values, api.debug)
if err != nil {
return nil, nil, err
}
@ -304,29 +237,24 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam
if params.InitialComment != "" {
values.Add("initial_comment", params.InitialComment)
}
if params.ThreadTimestamp != "" {
values.Add("thread_ts", params.ThreadTimestamp)
}
if len(params.Channels) != 0 {
values.Add("channels", strings.Join(params.Channels, ","))
}
if params.Content != "" {
values.Add("content", params.Content)
err = api.postMethod(ctx, "files.upload", values, response)
err = postForm(ctx, api.httpclient, SLACK_API+"files.upload", values, response, api.debug)
} else if params.File != "" {
err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", values, response, api)
err = postLocalWithMultipartResponse(ctx, api.httpclient, "files.upload", params.File, "file", values, response, api.debug)
} else if params.Reader != nil {
if params.Filename == "" {
return nil, fmt.Errorf("files.upload: FileUploadParameters.Filename is mandatory when using FileUploadParameters.Reader")
}
err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", values, params.Reader, response, api)
err = postWithMultipartResponse(ctx, api.httpclient, "files.upload", params.Filename, "file", values, params.Reader, response, api.debug)
}
if err != nil {
return nil, err
}
return &response.File, response.Err()
if !response.Ok {
return nil, errors.New(response.Error)
}
return &response.File, nil
}
// DeleteFileComment deletes a file's comment
@ -337,7 +265,7 @@ func (api *Client) DeleteFileComment(commentID, fileID string) error {
// DeleteFileCommentContext deletes a file's comment with a custom context
func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, commentID string) (err error) {
if fileID == "" || commentID == "" {
return ErrParametersMissing
return errors.New("received empty parameters")
}
values := url.Values{
@ -345,7 +273,7 @@ func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, comment
"file": {fileID},
"id": {commentID},
}
_, err = api.fileRequest(ctx, "files.comments.delete", values)
_, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api.debug)
return err
}
@ -361,7 +289,7 @@ func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err er
"file": {fileID},
}
_, err = api.fileRequest(ctx, "files.delete", values)
_, err = fileRequest(ctx, api.httpclient, "files.delete", values, api.debug)
return err
}
@ -377,7 +305,7 @@ func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string
"file": {fileID},
}
response, err := api.fileRequest(ctx, "files.revokePublicURL", values)
response, err := fileRequest(ctx, api.httpclient, "files.revokePublicURL", values, api.debug)
if err != nil {
return nil, err
}
@ -396,7 +324,7 @@ func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string)
"file": {fileID},
}
response, err := api.fileRequest(ctx, "files.sharedPublicURL", values)
response, err := fileRequest(ctx, api.httpclient, "files.sharedPublicURL", values, api.debug)
if err != nil {
return nil, nil, nil, err
}

View File

@ -2,13 +2,14 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
// Group contains all the information for a group
type Group struct {
GroupConversation
groupConversation
IsGroup bool `json:"is_group"`
}
@ -27,14 +28,16 @@ type groupResponseFull struct {
SlackResponse
}
func (api *Client) groupRequest(ctx context.Context, path string, values url.Values) (*groupResponseFull, error) {
func groupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*groupResponseFull, error) {
response := &groupResponseFull{}
err := api.postMethod(ctx, path, values, response)
err := postForm(ctx, client, SLACK_API+path, values, response, debug)
if err != nil {
return nil, err
}
return response, response.Err()
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// ArchiveGroup archives a private group
@ -49,7 +52,7 @@ func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error
"channel": {group},
}
_, err := api.groupRequest(ctx, "groups.archive", values)
_, err := groupRequest(ctx, api.httpclient, "groups.archive", values, api.debug)
return err
}
@ -65,7 +68,7 @@ func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) erro
"channel": {group},
}
_, err := api.groupRequest(ctx, "groups.unarchive", values)
_, err := groupRequest(ctx, api.httpclient, "groups.unarchive", values, api.debug)
return err
}
@ -81,7 +84,7 @@ func (api *Client) CreateGroupContext(ctx context.Context, group string) (*Group
"name": {group},
}
response, err := api.groupRequest(ctx, "groups.create", values)
response, err := groupRequest(ctx, api.httpclient, "groups.create", values, api.debug)
if err != nil {
return nil, err
}
@ -106,13 +109,32 @@ func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (*
"channel": {group},
}
response, err := api.groupRequest(ctx, "groups.createChild", values)
response, err := groupRequest(ctx, api.httpclient, "groups.createChild", values, api.debug)
if err != nil {
return nil, err
}
return &response.Group, nil
}
// CloseGroup closes a private group
func (api *Client) CloseGroup(group string) (bool, bool, error) {
return api.CloseGroupContext(context.Background(), group)
}
// CloseGroupContext closes a private group with a custom context
func (api *Client) CloseGroupContext(ctx context.Context, group string) (bool, bool, error) {
values := url.Values{
"token": {api.token},
"channel": {group},
}
response, err := imRequest(ctx, api.httpclient, "groups.close", values, api.debug)
if err != nil {
return false, false, err
}
return response.NoOp, response.AlreadyClosed, nil
}
// GetGroupHistory fetches all the history for a private group
func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*History, error) {
return api.GetGroupHistoryContext(context.Background(), group, params)
@ -148,7 +170,7 @@ func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, par
}
}
response, err := api.groupRequest(ctx, "groups.history", values)
response, err := groupRequest(ctx, api.httpclient, "groups.history", values, api.debug)
if err != nil {
return nil, err
}
@ -168,7 +190,7 @@ func (api *Client) InviteUserToGroupContext(ctx context.Context, group, user str
"user": {user},
}
response, err := api.groupRequest(ctx, "groups.invite", values)
response, err := groupRequest(ctx, api.httpclient, "groups.invite", values, api.debug)
if err != nil {
return nil, false, err
}
@ -187,7 +209,7 @@ func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err err
"channel": {group},
}
_, err = api.groupRequest(ctx, "groups.leave", values)
_, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug)
return err
}
@ -204,7 +226,7 @@ func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user str
"user": {user},
}
_, err = api.groupRequest(ctx, "groups.kick", values)
_, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug)
return err
}
@ -222,7 +244,7 @@ func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) (
values.Add("exclude_archived", "1")
}
response, err := api.groupRequest(ctx, "groups.list", values)
response, err := groupRequest(ctx, api.httpclient, "groups.list", values, api.debug)
if err != nil {
return nil, err
}
@ -237,12 +259,11 @@ func (api *Client) GetGroupInfo(group string) (*Group, error) {
// GetGroupInfoContext retrieves the given group with a custom context
func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Group, error) {
values := url.Values{
"token": {api.token},
"channel": {group},
"include_locale": {strconv.FormatBool(true)},
"token": {api.token},
"channel": {group},
}
response, err := api.groupRequest(ctx, "groups.info", values)
response, err := groupRequest(ctx, api.httpclient, "groups.info", values, api.debug)
if err != nil {
return nil, err
}
@ -267,7 +288,7 @@ func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string
"ts": {ts},
}
_, err = api.groupRequest(ctx, "groups.mark", values)
_, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug)
return err
}
@ -283,7 +304,7 @@ func (api *Client) OpenGroupContext(ctx context.Context, group string) (bool, bo
"channel": {group},
}
response, err := api.groupRequest(ctx, "groups.open", values)
response, err := groupRequest(ctx, api.httpclient, "groups.open", values, api.debug)
if err != nil {
return false, false, err
}
@ -307,7 +328,7 @@ func (api *Client) RenameGroupContext(ctx context.Context, group, name string) (
// 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 := api.groupRequest(ctx, "groups.rename", values)
response, err := groupRequest(ctx, api.httpclient, "groups.rename", values, api.debug)
if err != nil {
return nil, err
}
@ -327,7 +348,7 @@ func (api *Client) SetGroupPurposeContext(ctx context.Context, group, purpose st
"purpose": {purpose},
}
response, err := api.groupRequest(ctx, "groups.setPurpose", values)
response, err := groupRequest(ctx, api.httpclient, "groups.setPurpose", values, api.debug)
if err != nil {
return "", err
}
@ -347,7 +368,7 @@ func (api *Client) SetGroupTopicContext(ctx context.Context, group, topic string
"topic": {topic},
}
response, err := api.groupRequest(ctx, "groups.setTopic", values)
response, err := groupRequest(ctx, api.httpclient, "groups.setTopic", values, api.debug)
if err != nil {
return "", err
}

View File

@ -2,6 +2,7 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
@ -22,18 +23,22 @@ type imResponseFull struct {
// IM contains information related to the Direct Message channel
type IM struct {
Conversation
IsUserDeleted bool `json:"is_user_deleted"`
conversation
IsIM bool `json:"is_im"`
User string `json:"user"`
IsUserDeleted bool `json:"is_user_deleted"`
}
func (api *Client) imRequest(ctx context.Context, path string, values url.Values) (*imResponseFull, error) {
func imRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*imResponseFull, error) {
response := &imResponseFull{}
err := api.postMethod(ctx, path, values, response)
err := postSlackMethod(ctx, client, path, values, response, debug)
if err != nil {
return nil, err
}
return response, response.Err()
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// CloseIMChannel closes the direct message channel
@ -48,7 +53,7 @@ func (api *Client) CloseIMChannelContext(ctx context.Context, channel string) (b
"channel": {channel},
}
response, err := api.imRequest(ctx, "im.close", values)
response, err := imRequest(ctx, api.httpclient, "im.close", values, api.debug)
if err != nil {
return false, false, err
}
@ -69,7 +74,7 @@ func (api *Client) OpenIMChannelContext(ctx context.Context, user string) (bool,
"user": {user},
}
response, err := api.imRequest(ctx, "im.open", values)
response, err := imRequest(ctx, api.httpclient, "im.open", values, api.debug)
if err != nil {
return false, false, "", err
}
@ -89,7 +94,7 @@ func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string)
"ts": {ts},
}
_, err := api.imRequest(ctx, "im.mark", values)
_, err := imRequest(ctx, api.httpclient, "im.mark", values, api.debug)
return err
}
@ -128,7 +133,7 @@ func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, para
}
}
response, err := api.imRequest(ctx, "im.history", values)
response, err := imRequest(ctx, api.httpclient, "im.history", values, api.debug)
if err != nil {
return nil, err
}
@ -146,7 +151,7 @@ func (api *Client) GetIMChannelsContext(ctx context.Context) ([]IM, error) {
"token": {api.token},
}
response, err := api.imRequest(ctx, "im.list", values)
response, err := imRequest(ctx, api.httpclient, "im.list", values, api.debug)
if err != nil {
return nil, err
}

225
vendor/github.com/nlopes/slack/info.go generated vendored Normal file
View File

@ -0,0 +1,225 @@
package slack
import (
"bytes"
"fmt"
"strconv"
"time"
)
// UserPrefs needs to be implemented
type UserPrefs struct {
// "highlight_words":"",
// "user_colors":"",
// "color_names_in_list":true,
// "growls_enabled":true,
// "tz":"Europe\/London",
// "push_dm_alert":true,
// "push_mention_alert":true,
// "push_everything":true,
// "push_idle_wait":2,
// "push_sound":"b2.mp3",
// "push_loud_channels":"",
// "push_mention_channels":"",
// "push_loud_channels_set":"",
// "email_alerts":"instant",
// "email_alerts_sleep_until":0,
// "email_misc":false,
// "email_weekly":true,
// "welcome_message_hidden":false,
// "all_channels_loud":true,
// "loud_channels":"",
// "never_channels":"",
// "loud_channels_set":"",
// "show_member_presence":true,
// "search_sort":"timestamp",
// "expand_inline_imgs":true,
// "expand_internal_inline_imgs":true,
// "expand_snippets":false,
// "posts_formatting_guide":true,
// "seen_welcome_2":true,
// "seen_ssb_prompt":false,
// "search_only_my_channels":false,
// "emoji_mode":"default",
// "has_invited":true,
// "has_uploaded":false,
// "has_created_channel":true,
// "search_exclude_channels":"",
// "messages_theme":"default",
// "webapp_spellcheck":true,
// "no_joined_overlays":false,
// "no_created_overlays":true,
// "dropbox_enabled":false,
// "seen_user_menu_tip_card":true,
// "seen_team_menu_tip_card":true,
// "seen_channel_menu_tip_card":true,
// "seen_message_input_tip_card":true,
// "seen_channels_tip_card":true,
// "seen_domain_invite_reminder":false,
// "seen_member_invite_reminder":false,
// "seen_flexpane_tip_card":true,
// "seen_search_input_tip_card":true,
// "mute_sounds":false,
// "arrow_history":false,
// "tab_ui_return_selects":true,
// "obey_inline_img_limit":true,
// "new_msg_snd":"knock_brush.mp3",
// "collapsible":false,
// "collapsible_by_click":true,
// "require_at":false,
// "mac_ssb_bounce":"",
// "mac_ssb_bullet":true,
// "win_ssb_bullet":true,
// "expand_non_media_attachments":true,
// "show_typing":true,
// "pagekeys_handled":true,
// "last_snippet_type":"",
// "display_real_names_override":0,
// "time24":false,
// "enter_is_special_in_tbt":false,
// "graphic_emoticons":false,
// "convert_emoticons":true,
// "autoplay_chat_sounds":true,
// "ss_emojis":true,
// "sidebar_behavior":"",
// "mark_msgs_read_immediately":true,
// "start_scroll_at_oldest":true,
// "snippet_editor_wrap_long_lines":false,
// "ls_disabled":false,
// "sidebar_theme":"default",
// "sidebar_theme_custom_values":"",
// "f_key_search":false,
// "k_key_omnibox":true,
// "speak_growls":false,
// "mac_speak_voice":"com.apple.speech.synthesis.voice.Alex",
// "mac_speak_speed":250,
// "comma_key_prefs":false,
// "at_channel_suppressed_channels":"",
// "push_at_channel_suppressed_channels":"",
// "prompted_for_email_disabling":false,
// "full_text_extracts":false,
// "no_text_in_notifications":false,
// "muted_channels":"",
// "no_macssb1_banner":false,
// "privacy_policy_seen":true,
// "search_exclude_bots":false,
// "fuzzy_matching":false
}
// UserDetails contains user details coming in the initial response from StartRTM
type UserDetails struct {
ID string `json:"id"`
Name string `json:"name"`
Created JSONTime `json:"created"`
ManualPresence string `json:"manual_presence"`
Prefs UserPrefs `json:"prefs"`
}
// JSONTime exists so that we can have a String method converting the date
type JSONTime int64
// String converts the unix timestamp into a string
func (t JSONTime) String() string {
tm := t.Time()
return fmt.Sprintf("\"%s\"", tm.Format("Mon Jan _2"))
}
// Time returns a `time.Time` representation of this value.
func (t JSONTime) Time() time.Time {
return time.Unix(int64(t), 0)
}
// UnmarshalJSON will unmarshal both string and int JSON values
func (t *JSONTime) UnmarshalJSON(buf []byte) error {
s := bytes.Trim(buf, `"`)
v, err := strconv.Atoi(string(s))
if err != nil {
return err
}
*t = JSONTime(int64(v))
return nil
}
// Team contains details about a team
type Team struct {
ID string `json:"id"`
Name string `json:"name"`
Domain string `json:"domain"`
}
// Icons XXX: needs further investigation
type Icons struct {
Image36 string `json:"image_36,omitempty"`
Image48 string `json:"image_48,omitempty"`
Image72 string `json:"image_72,omitempty"`
}
// Info contains various details about Users, Channels, Bots and the authenticated user.
// It is returned by StartRTM or included in the "ConnectedEvent" RTM event.
type Info struct {
URL string `json:"url,omitempty"`
User *UserDetails `json:"self,omitempty"`
Team *Team `json:"team,omitempty"`
Users []User `json:"users,omitempty"`
Channels []Channel `json:"channels,omitempty"`
Groups []Group `json:"groups,omitempty"`
Bots []Bot `json:"bots,omitempty"`
IMs []IM `json:"ims,omitempty"`
}
type infoResponseFull struct {
Info
SlackResponse
}
// GetBotByID returns a bot given a bot id
func (info Info) GetBotByID(botID string) *Bot {
for _, bot := range info.Bots {
if bot.ID == botID {
return &bot
}
}
return nil
}
// GetUserByID returns a user given a user id
func (info Info) GetUserByID(userID string) *User {
for _, user := range info.Users {
if user.ID == userID {
return &user
}
}
return nil
}
// GetChannelByID returns a channel given a channel id
func (info Info) GetChannelByID(channelID string) *Channel {
for _, channel := range info.Channels {
if channel.ID == channelID {
return &channel
}
}
return nil
}
// GetGroupByID returns a group given a group id
func (info Info) GetGroupByID(groupID string) *Group {
for _, group := range info.Groups {
if group.ID == groupID {
return &group
}
}
return nil
}
// GetIMByID returns an IM given an IM id
func (info Info) GetIMByID(imID string) *IM {
for _, im := range info.IMs {
if im.ID == imID {
return &im
}
}
return nil
}

53
vendor/github.com/nlopes/slack/logger.go generated vendored Normal file
View File

@ -0,0 +1,53 @@
package slack
import (
"fmt"
"sync"
)
// SetLogger let's library users supply a logger, so that api debugging
// can be logged along with the application's debugging info.
func SetLogger(l logProvider) {
loggerMutex.Lock()
logger = ilogger{logProvider: l}
loggerMutex.Unlock()
}
var (
loggerMutex = new(sync.Mutex)
logger logInternal // A logger that can be set by consumers
)
// logProvider is a logger interface compatible with both stdlib and some
// 3rd party loggers such as logrus.
type logProvider interface {
Output(int, string) error
}
// logInternal represents the internal logging api we use.
type logInternal interface {
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
Output(int, string) error
}
// ilogger implements the additional methods used by our internal logging.
type ilogger struct {
logProvider
}
// Println replicates the behaviour of the standard logger.
func (t ilogger) Println(v ...interface{}) {
t.Output(2, fmt.Sprintln(v...))
}
// Printf replicates the behaviour of the standard logger.
func (t ilogger) Printf(format string, v ...interface{}) {
t.Output(2, fmt.Sprintf(format, v...))
}
// Print replicates the behaviour of the standard logger.
func (t ilogger) Print(v ...interface{}) {
t.Output(2, fmt.Sprint(v...))
}

View File

@ -4,25 +4,22 @@ package slack
type OutgoingMessage struct {
ID int `json:"id"`
// channel ID
Channel string `json:"channel,omitempty"`
Text string `json:"text,omitempty"`
Type string `json:"type,omitempty"`
ThreadTimestamp string `json:"thread_ts,omitempty"`
ThreadBroadcast bool `json:"reply_broadcast,omitempty"`
IDs []string `json:"ids,omitempty"`
Channel string `json:"channel,omitempty"`
Text string `json:"text,omitempty"`
Type string `json:"type,omitempty"`
ThreadTimestamp string `json:"thread_ts,omitempty"`
ThreadBroadcast bool `json:"reply_broadcast,omitempty"`
}
// Message is an auxiliary type to allow us to have a message containing sub messages
type Message struct {
Msg
SubMessage *Msg `json:"message,omitempty"`
PreviousMessage *Msg `json:"previous_message,omitempty"`
SubMessage *Msg `json:"message,omitempty"`
}
// Msg contains information about a slack message
type Msg struct {
// Basic Message
ClientMsgID string `json:"client_msg_id"`
Type string `json:"type,omitempty"`
Channel string `json:"channel,omitempty"`
User string `json:"user,omitempty"`
@ -94,18 +91,8 @@ type Msg struct {
ResponseType string `json:"response_type,omitempty"`
ReplaceOriginal bool `json:"replace_original"`
DeleteOriginal bool `json:"delete_original"`
// Block type Message
Blocks Blocks `json:"blocks,omitempty"`
}
const (
// ResponseTypeInChannel in channel response for slash commands.
ResponseTypeInChannel = "in_channel"
// ResponseTypeEphemeral ephemeral response for slash commands.
ResponseTypeEphemeral = "ephemeral"
)
// Icon is used for bot messages
type Icon struct {
IconURL string `json:"icon_url,omitempty"`
@ -160,15 +147,6 @@ func (rtm *RTM) NewOutgoingMessage(text string, channelID string, options ...RTM
return &msg
}
// NewSubscribeUserPresence prepares an OutgoingMessage that the user can
// use to subscribe presence events for the specified users.
func (rtm *RTM) NewSubscribeUserPresence(ids []string) *OutgoingMessage {
return &OutgoingMessage{
Type: "presence_sub",
IDs: ids,
}
}
// NewTypingMessage prepares an OutgoingMessage that the user can
// use to send as a typing indicator. Use this function to properly set the
// messageID.
@ -196,4 +174,5 @@ func RTMsgOptionBroadcast() RTMsgOption {
return func(msg *OutgoingMessage) {
msg.ThreadBroadcast = true
}
}

240
vendor/github.com/nlopes/slack/misc.go generated vendored Normal file
View File

@ -0,0 +1,240 @@
package slack
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
type SlackResponse struct {
Ok bool `json:"ok"`
Error string `json:"error"`
}
func (t SlackResponse) Err() error {
if t.Ok {
return nil
}
// handle pure text based responses like chat.post
// which while they have a slack response in their data structure
// it doesn't actually get set during parsing.
if strings.TrimSpace(t.Error) == "" {
return nil
}
return errors.New(t.Error)
}
// StatusCodeError represents an http response error.
// type httpStatusCode interface { HTTPStatusCode() int } to handle it.
type statusCodeError struct {
Code int
Status string
}
func (t statusCodeError) Error() string {
// TODO: this is a bad error string, should clean it up with a breaking changes
// merger.
return fmt.Sprintf("Slack server error: %s.", t.Status)
}
func (t statusCodeError) HTTPStatusCode() int {
return t.Code
}
type RateLimitedError struct {
RetryAfter time.Duration
}
func (e *RateLimitedError) Error() string {
return fmt.Sprintf("Slack rate limit exceeded, retry after %s", e.RetryAfter)
}
func fileUploadReq(ctx context.Context, path, fieldname, filename string, values url.Values, r io.Reader) (*http.Request, error) {
body := &bytes.Buffer{}
wr := multipart.NewWriter(body)
ioWriter, err := wr.CreateFormFile(fieldname, filename)
if err != nil {
wr.Close()
return nil, err
}
_, err = io.Copy(ioWriter, r)
if err != nil {
wr.Close()
return nil, err
}
// Close the multipart writer or the footer won't be written
wr.Close()
req, err := http.NewRequest("POST", path, body)
req = req.WithContext(ctx)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", wr.FormDataContentType())
req.URL.RawQuery = (values).Encode()
return req, nil
}
func parseResponseBody(body io.ReadCloser, intf interface{}, debug bool) error {
response, err := ioutil.ReadAll(body)
if err != nil {
return err
}
// FIXME: will be api.Debugf
if debug {
logger.Printf("parseResponseBody: %s\n", string(response))
}
return json.Unmarshal(response, intf)
}
func postLocalWithMultipartResponse(ctx context.Context, client HTTPRequester, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error {
fullpath, err := filepath.Abs(fpath)
if err != nil {
return err
}
file, err := os.Open(fullpath)
if err != nil {
return err
}
defer file.Close()
return postWithMultipartResponse(ctx, client, path, filepath.Base(fpath), fieldname, values, file, intf, debug)
}
func postWithMultipartResponse(ctx context.Context, client HTTPRequester, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, debug bool) error {
req, err := fileUploadReq(ctx, SLACK_API+path, fieldname, name, values, r)
if err != nil {
return err
}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != http.StatusOK {
logResponse(resp, debug)
return statusCodeError{Code: resp.StatusCode, Status: resp.Status}
}
return parseResponseBody(resp.Body, intf, debug)
}
func doPost(ctx context.Context, client HTTPRequester, req *http.Request, intf interface{}, debug bool) error {
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != http.StatusOK {
logResponse(resp, debug)
return statusCodeError{Code: resp.StatusCode, Status: resp.Status}
}
return parseResponseBody(resp.Body, intf, debug)
}
// post JSON.
func postJSON(ctx context.Context, client HTTPRequester, endpoint, token string, json []byte, intf interface{}, debug bool) error {
reqBody := bytes.NewBuffer(json)
req, err := http.NewRequest("POST", endpoint, reqBody)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return doPost(ctx, client, req, intf, debug)
}
// post a url encoded form.
func postForm(ctx context.Context, client HTTPRequester, endpoint string, values url.Values, intf interface{}, debug bool) error {
reqBody := strings.NewReader(values.Encode())
req, err := http.NewRequest("POST", endpoint, reqBody)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return doPost(ctx, client, req, intf, debug)
}
// post to a slack web method.
func postSlackMethod(ctx context.Context, client HTTPRequester, path string, values url.Values, intf interface{}, debug bool) error {
return postForm(ctx, client, SLACK_API+path, values, intf, debug)
}
func parseAdminResponse(ctx context.Context, client HTTPRequester, method string, teamName string, values url.Values, intf interface{}, debug bool) error {
endpoint := fmt.Sprintf(SLACK_WEB_API_FORMAT, teamName, method, time.Now().Unix())
return postForm(ctx, client, endpoint, values, intf, debug)
}
func logResponse(resp *http.Response, debug bool) error {
if debug {
text, err := httputil.DumpResponse(resp, true)
if err != nil {
return err
}
logger.Print(string(text))
}
return nil
}
func okJSONHandler(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
response, _ := json.Marshal(SlackResponse{
Ok: true,
})
rw.Write(response)
}
type errorString string
func (t errorString) Error() string {
return string(t)
}
// timerReset safely reset a timer, see time.Timer.Reset for details.
func timerReset(t *time.Timer, d time.Duration) {
if !t.Stop() {
<-t.C
}
t.Reset(d)
}

66
vendor/github.com/nlopes/slack/oauth.go generated vendored Normal file
View File

@ -0,0 +1,66 @@
package slack
import (
"context"
"errors"
"net/url"
)
type OAuthResponseIncomingWebhook struct {
URL string `json:"url"`
Channel string `json:"channel"`
ChannelID string `json:"channel_id,omitempty"`
ConfigurationURL string `json:"configuration_url"`
}
type OAuthResponseBot struct {
BotUserID string `json:"bot_user_id"`
BotAccessToken string `json:"bot_access_token"`
}
type OAuthResponse struct {
AccessToken string `json:"access_token"`
Scope string `json:"scope"`
TeamName string `json:"team_name"`
TeamID string `json:"team_id"`
IncomingWebhook OAuthResponseIncomingWebhook `json:"incoming_webhook"`
Bot OAuthResponseBot `json:"bot"`
UserID string `json:"user_id,omitempty"`
SlackResponse
}
// GetOAuthToken retrieves an AccessToken
func GetOAuthToken(clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) {
return GetOAuthTokenContext(context.Background(), clientID, clientSecret, code, redirectURI, debug)
}
// GetOAuthTokenContext retrieves an AccessToken with a custom context
func GetOAuthTokenContext(ctx context.Context, clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) {
response, err := GetOAuthResponseContext(ctx, clientID, clientSecret, code, redirectURI, debug)
if err != nil {
return "", "", err
}
return response.AccessToken, response.Scope, nil
}
func GetOAuthResponse(clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) {
return GetOAuthResponseContext(context.Background(), clientID, clientSecret, code, redirectURI, debug)
}
func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) {
values := url.Values{
"client_id": {clientID},
"client_secret": {clientSecret},
"code": {code},
"redirect_uri": {redirectURI},
}
response := &OAuthResponse{}
err = postSlackMethod(ctx, customHTTPClient, "oauth.access", values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}

View File

@ -34,7 +34,7 @@ func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemR
}
response := &SlackResponse{}
if err := api.postMethod(ctx, "pins.add", values, response); err != nil {
if err := postSlackMethod(ctx, api.httpclient, "pins.add", values, response, api.debug); err != nil {
return err
}
@ -63,7 +63,7 @@ func (api *Client) RemovePinContext(ctx context.Context, channel string, item It
}
response := &SlackResponse{}
if err := api.postMethod(ctx, "pins.remove", values, response); err != nil {
if err := postSlackMethod(ctx, api.httpclient, "pins.remove", values, response, api.debug); err != nil {
return err
}
@ -83,7 +83,7 @@ func (api *Client) ListPinsContext(ctx context.Context, channel string) ([]Item,
}
response := &listPinsResponseFull{}
err := api.postMethod(ctx, "pins.list", values, response)
err := postSlackMethod(ctx, api.httpclient, "pins.list", values, response, api.debug)
if err != nil {
return nil, nil, err
}

View File

@ -2,6 +2,7 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
@ -154,7 +155,7 @@ func (api *Client) AddReactionContext(ctx context.Context, name string, item Ite
}
response := &SlackResponse{}
if err := api.postMethod(ctx, "reactions.add", values, response); err != nil {
if err := postSlackMethod(ctx, api.httpclient, "reactions.add", values, response, api.debug); err != nil {
return err
}
@ -188,7 +189,7 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item
}
response := &SlackResponse{}
if err := api.postMethod(ctx, "reactions.remove", values, response); err != nil {
if err := postSlackMethod(ctx, api.httpclient, "reactions.remove", values, response, api.debug); err != nil {
return err
}
@ -222,14 +223,12 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params
}
response := &getReactionsResponseFull{}
if err := api.postMethod(ctx, "reactions.get", values, response); err != nil {
if err := postSlackMethod(ctx, api.httpclient, "reactions.get", values, response, api.debug); err != nil {
return nil, err
}
if err := response.Err(); err != nil {
return nil, err
if !response.Ok {
return nil, errors.New(response.Error)
}
return response.extractReactions(), nil
}
@ -257,14 +256,12 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction
}
response := &listReactionsResponseFull{}
err := api.postMethod(ctx, "reactions.list", values, response)
err := postSlackMethod(ctx, api.httpclient, "reactions.list", values, response, api.debug)
if err != nil {
return nil, nil, err
}
if err := response.Err(); err != nil {
return nil, nil, err
if !response.Ok {
return nil, nil, errors.New(response.Error)
}
return response.extractReactedItems(), &response.Paging, nil
}

View File

@ -2,6 +2,7 @@ package slack
import (
"context"
"encoding/json"
"net/url"
"sync"
"time"
@ -37,7 +38,7 @@ func (api *Client) StartRTM() (info *Info, websocketURL string, err error) {
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) {
response := &infoResponseFull{}
err = api.postMethod(ctx, "rtm.start", url.Values{"token": {api.token}}, response)
err = postSlackMethod(ctx, api.httpclient, "rtm.start", url.Values{"token": {api.token}}, response, api.debug)
if err != nil {
return nil, "", err
}
@ -62,7 +63,7 @@ func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) {
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) {
response := &infoResponseFull{}
err = api.postMethod(ctx, "rtm.connect", url.Values{"token": {api.token}}, response)
err = postSlackMethod(ctx, api.httpclient, "rtm.connect", url.Values{"token": {api.token}}, response, api.debug)
if err != nil {
api.Debugf("Failed to connect to RTM: %s", err)
return nil, "", err
@ -99,13 +100,6 @@ func RTMOptionPingInterval(d time.Duration) RTMOption {
}
}
// RTMOptionConnParams installs parameters to embed into the connection URL.
func RTMOptionConnParams(connParams url.Values) RTMOption {
return func(rtm *RTM) {
rtm.connParams = connParams
}
}
// NewRTM returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol.
func (api *Client) NewRTM(options ...RTMOption) *RTM {
@ -115,10 +109,12 @@ func (api *Client) NewRTM(options ...RTMOption) *RTM {
outgoingMessages: make(chan OutgoingMessage, 20),
pingInterval: defaultPingInterval,
pingDeadman: time.NewTimer(deadmanDuration(defaultPingInterval)),
isConnected: false,
wasIntentional: true,
killChannel: make(chan bool),
disconnected: make(chan struct{}),
disconnectedm: &sync.Once{},
disconnected: make(chan struct{}, 1),
forcePing: make(chan bool),
rawEvents: make(chan json.RawMessage),
idGen: NewSafeID(1),
mu: &sync.Mutex{},
}
@ -129,3 +125,14 @@ func (api *Client) NewRTM(options ...RTMOption) *RTM {
return result
}
// NewRTMWithOptions Deprecated just use NewRTM(RTMOptionsUseStart(true))
// returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol.
// This also allows to configure various options available for RTM API.
func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM {
if options != nil {
return api.NewRTM(RTMOptionUseStart(options.UseRTMStart))
}
return api.NewRTM()
}

View File

@ -2,6 +2,7 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
@ -23,14 +24,8 @@ type SearchParameters struct {
}
type CtxChannel struct {
ID string `json:"id"`
Name string `json:"name"`
IsExtShared bool `json:"is_ext_shared"`
IsMPIM bool `json:"is_mpim"`
ISOrgShared bool `json:"is_org_shared"`
IsPendingExtShared bool `json:"is_pending_ext_shared"`
IsPrivate bool `json:"is_private"`
IsShared bool `json:"is_shared"`
ID string `json:"id"`
Name string `json:"name"`
}
type CtxMessage struct {
@ -47,7 +42,6 @@ type SearchMessage struct {
User string `json:"user"`
Username string `json:"username"`
Timestamp string `json:"ts"`
Blocks Blocks `json:"blocks,omitempty"`
Text string `json:"text"`
Permalink string `json:"permalink"`
Attachments []Attachment `json:"attachments"`
@ -110,12 +104,14 @@ func (api *Client) _search(ctx context.Context, path, query string, params Searc
}
response = &searchResponseFull{}
err := api.postMethod(ctx, path, values, response)
err := postSlackMethod(ctx, api.httpclient, path, values, response, api.debug)
if err != nil {
return nil, err
}
return response, response.Err()
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}

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

@ -0,0 +1,140 @@
package slack
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"net/url"
"os"
)
// Added as a var so that we can change this for testing purposes
var SLACK_API string = "https://slack.com/api/"
var SLACK_WEB_API_FORMAT string = "https://%s.slack.com/api/users.admin.%s?t=%s"
// HTTPClient sets a custom http.Client
// deprecated: in favor of SetHTTPClient()
var HTTPClient = &http.Client{}
var customHTTPClient HTTPRequester = HTTPClient
// HTTPRequester defines the minimal interface needed for an http.Client to be implemented.
//
// Use it in conjunction with the SetHTTPClient function to allow for other capabilities
// like a tracing http.Client
type HTTPRequester interface {
Do(*http.Request) (*http.Response, error)
}
// SetHTTPClient allows you to specify a custom http.Client
// Use this instead of the package level HTTPClient variable if you want to use a custom client like the
// Stackdriver Trace HTTPClient https://godoc.org/cloud.google.com/go/trace#HTTPClient
func SetHTTPClient(client HTTPRequester) {
customHTTPClient = client
}
// ResponseMetadata holds pagination metadata
type ResponseMetadata struct {
Cursor string `json:"next_cursor"`
}
func (t *ResponseMetadata) initialize() *ResponseMetadata {
if t != nil {
return t
}
return &ResponseMetadata{}
}
type AuthTestResponse struct {
URL string `json:"url"`
Team string `json:"team"`
User string `json:"user"`
TeamID string `json:"team_id"`
UserID string `json:"user_id"`
}
type authTestResponseFull struct {
SlackResponse
AuthTestResponse
}
type Client struct {
token string
info Info
debug bool
httpclient HTTPRequester
}
// Option defines an option for a Client
type Option func(*Client)
// OptionHTTPClient - provide a custom http client to the slack client.
func OptionHTTPClient(c HTTPRequester) func(*Client) {
return func(s *Client) {
s.httpclient = c
}
}
// New builds a slack client from the provided token and options.
func New(token string, options ...Option) *Client {
s := &Client{
token: token,
httpclient: customHTTPClient,
}
for _, opt := range options {
opt(s)
}
return s
}
// AuthTest tests if the user is able to do authenticated requests or not
func (api *Client) AuthTest() (response *AuthTestResponse, error error) {
return api.AuthTestContext(context.Background())
}
// AuthTestContext tests if the user is able to do authenticated requests or not with a custom context
func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestResponse, error error) {
api.Debugf("Challenging auth...")
responseFull := &authTestResponseFull{}
err := postSlackMethod(ctx, api.httpclient, "auth.test", url.Values{"token": {api.token}}, responseFull, api.debug)
if err != nil {
api.Debugf("failed to test for auth: %s", err)
return nil, err
}
if !responseFull.Ok {
api.Debugf("auth response was not Ok: %s", responseFull.Error)
return nil, errors.New(responseFull.Error)
}
api.Debugf("Auth challenge was successful with response %+v", responseFull.AuthTestResponse)
return &responseFull.AuthTestResponse, nil
}
// SetDebug switches the api into debug mode
// When in debug mode, it logs various info about what its doing
// If you ever use this in production, don't call SetDebug(true)
func (api *Client) SetDebug(debug bool) {
api.debug = debug
if debug && logger == nil {
SetLogger(log.New(os.Stdout, "nlopes/slack", log.LstdFlags|log.Lshortfile))
}
}
// Debugf print a formatted debug line.
func (api *Client) Debugf(format string, v ...interface{}) {
if api.debug {
logger.Output(2, fmt.Sprintf(format, v...))
}
}
// Debugln print a debug line.
func (api *Client) Debugln(v ...interface{}) {
if api.debug {
logger.Output(2, fmt.Sprintln(v...))
}
}

View File

@ -2,9 +2,9 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
"time"
)
const (
@ -58,7 +58,7 @@ func (api *Client) AddStarContext(ctx context.Context, channel string, item Item
}
response := &SlackResponse{}
if err := api.postMethod(ctx, "stars.add", values, response); err != nil {
if err := postSlackMethod(ctx, api.httpclient, "stars.add", values, response, api.debug); err != nil {
return err
}
@ -87,7 +87,7 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I
}
response := &SlackResponse{}
if err := api.postMethod(ctx, "stars.remove", values, response); err != nil {
if err := postSlackMethod(ctx, api.httpclient, "stars.remove", values, response, api.debug); err != nil {
return err
}
@ -115,15 +115,13 @@ func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters)
}
response := &listResponseFull{}
err := api.postMethod(ctx, "stars.list", values, response)
err := postSlackMethod(ctx, api.httpclient, "stars.list", values, response, api.debug)
if err != nil {
return nil, nil, err
}
if err := response.Err(); err != nil {
return nil, nil, err
if !response.Ok {
return nil, nil, errors.New(response.Error)
}
return response.Items, &response.Paging, nil
}
@ -159,105 +157,3 @@ func (api *Client) GetStarredContext(ctx context.Context, params StarsParameters
}
return starredItems, paging, nil
}
type listResponsePaginated struct {
Items []Item `json:"items"`
SlackResponse
Metadata ResponseMetadata `json:"response_metadata"`
}
// StarredItemPagination allows for paginating over the starred items
type StarredItemPagination struct {
Items []Item
limit int
previousResp *ResponseMetadata
c *Client
}
// ListStarsOption options for the GetUsers method call.
type ListStarsOption func(*StarredItemPagination)
// ListAllStars returns the complete list of starred items
func (api *Client) ListAllStars() ([]Item, error) {
return api.ListAllStarsContext(context.Background())
}
// ListAllStarsContext returns the list of users (with their detailed information) with a custom context
func (api *Client) ListAllStarsContext(ctx context.Context) (results []Item, err error) {
p := api.ListStarsPaginated()
for err == nil {
p, err = p.next(ctx)
if err == nil {
results = append(results, p.Items...)
} else if rateLimitedError, ok := err.(*RateLimitedError); ok {
select {
case <-ctx.Done():
err = ctx.Err()
case <-time.After(rateLimitedError.RetryAfter):
err = nil
}
}
}
return results, p.failure(err)
}
// ListStarsPaginated fetches users in a paginated fashion, see ListStarsPaginationContext for usage.
func (api *Client) ListStarsPaginated(options ...ListStarsOption) StarredItemPagination {
return newStarPagination(api, options...)
}
func newStarPagination(c *Client, options ...ListStarsOption) (sip StarredItemPagination) {
sip = StarredItemPagination{
c: c,
limit: 200, // per slack api documentation.
}
for _, opt := range options {
opt(&sip)
}
return sip
}
// done checks if the pagination has completed
func (StarredItemPagination) done(err error) bool {
return err == errPaginationComplete
}
// done checks if pagination failed.
func (t StarredItemPagination) failure(err error) error {
if t.done(err) {
return nil
}
return err
}
// next gets the next list of starred items based on the cursor value
func (t StarredItemPagination) next(ctx context.Context) (_ StarredItemPagination, err error) {
var (
resp *listResponsePaginated
)
if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") {
return t, errPaginationComplete
}
t.previousResp = t.previousResp.initialize()
values := url.Values{
"limit": {strconv.Itoa(t.limit)},
"token": {t.c.token},
"cursor": {t.previousResp.Cursor},
}
if err = t.c.postMethod(ctx, "stars.list", values, &resp); err != nil {
return t, err
}
t.previousResp = &resp.Metadata
t.Items = resp.Items
return t, nil
}

View File

@ -2,6 +2,7 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
@ -66,33 +67,44 @@ func NewAccessLogParameters() AccessLogParameters {
}
}
func (api *Client) teamRequest(ctx context.Context, path string, values url.Values) (*TeamResponse, error) {
func teamRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*TeamResponse, error) {
response := &TeamResponse{}
err := api.postMethod(ctx, path, values, response)
err := postSlackMethod(ctx, client, path, values, response, debug)
if err != nil {
return nil, err
}
return response, response.Err()
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
func (api *Client) billableInfoRequest(ctx context.Context, path string, values url.Values) (map[string]BillingActive, error) {
func billableInfoRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (map[string]BillingActive, error) {
response := &BillableInfoResponse{}
err := api.postMethod(ctx, path, values, response)
err := postSlackMethod(ctx, client, path, values, response, debug)
if err != nil {
return nil, err
}
return response.BillableInfo, response.Err()
if !response.Ok {
return nil, errors.New(response.Error)
}
return response.BillableInfo, nil
}
func (api *Client) accessLogsRequest(ctx context.Context, path string, values url.Values) (*LoginResponse, error) {
func accessLogsRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*LoginResponse, error) {
response := &LoginResponse{}
err := api.postMethod(ctx, path, values, response)
err := postSlackMethod(ctx, client, path, values, response, debug)
if err != nil {
return nil, err
}
return response, response.Err()
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// GetTeamInfo gets the Team Information of the user
@ -106,7 +118,7 @@ func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) {
"token": {api.token},
}
response, err := api.teamRequest(ctx, "team.info", values)
response, err := teamRequest(ctx, api.httpclient, "team.info", values, api.debug)
if err != nil {
return nil, err
}
@ -130,26 +142,24 @@ func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogPar
values.Add("page", strconv.Itoa(params.Page))
}
response, err := api.accessLogsRequest(ctx, "team.accessLogs", values)
response, err := accessLogsRequest(ctx, api.httpclient, "team.accessLogs", values, api.debug)
if err != nil {
return nil, nil, err
}
return response.Logins, &response.Paging, nil
}
// GetBillableInfo ...
func (api *Client) GetBillableInfo(user string) (map[string]BillingActive, error) {
return api.GetBillableInfoContext(context.Background(), user)
}
// GetBillableInfoContext ...
func (api *Client) GetBillableInfoContext(ctx context.Context, user string) (map[string]BillingActive, error) {
values := url.Values{
"token": {api.token},
"user": {user},
}
return api.billableInfoRequest(ctx, "team.billableInfo", values)
return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug)
}
// GetBillableInfoForTeam returns the billing_active status of all users on the team.
@ -163,5 +173,5 @@ func (api *Client) GetBillableInfoForTeamContext(ctx context.Context) (map[strin
"token": {api.token},
}
return api.billableInfoRequest(ctx, "team.billableInfo", values)
return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug)
}

View File

@ -2,6 +2,7 @@ package slack
import (
"context"
"errors"
"net/url"
"strings"
)
@ -24,7 +25,6 @@ type UserGroup struct {
DeletedBy string `json:"deleted_by"`
Prefs UserGroupPrefs `json:"prefs"`
UserCount int `json:"user_count"`
Users []string `json:"users"`
}
// UserGroupPrefs contains default channels and groups (private channels)
@ -40,14 +40,16 @@ type userGroupResponseFull struct {
SlackResponse
}
func (api *Client) userGroupRequest(ctx context.Context, path string, values url.Values) (*userGroupResponseFull, error) {
func userGroupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userGroupResponseFull, error) {
response := &userGroupResponseFull{}
err := api.postMethod(ctx, path, values, response)
err := postSlackMethod(ctx, client, path, values, response, debug)
if err != nil {
return nil, err
}
return response, response.Err()
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// CreateUserGroup creates a new user group
@ -74,7 +76,7 @@ func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGro
values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")}
}
response, err := api.userGroupRequest(ctx, "usergroups.create", values)
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.create", values, api.debug)
if err != nil {
return UserGroup{}, err
}
@ -93,7 +95,7 @@ func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string
"usergroup": {userGroup},
}
response, err := api.userGroupRequest(ctx, "usergroups.disable", values)
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.disable", values, api.debug)
if err != nil {
return UserGroup{}, err
}
@ -112,71 +114,25 @@ func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string)
"usergroup": {userGroup},
}
response, err := api.userGroupRequest(ctx, "usergroups.enable", values)
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.enable", values, api.debug)
if err != nil {
return UserGroup{}, err
}
return response.UserGroup, nil
}
// GetUserGroupsOption options for the GetUserGroups method call.
type GetUserGroupsOption func(*GetUserGroupsParams)
// GetUserGroupsOptionIncludeCount include the number of users in each User Group (default: false)
func GetUserGroupsOptionIncludeCount(b bool) GetUserGroupsOption {
return func(params *GetUserGroupsParams) {
params.IncludeCount = b
}
}
// GetUserGroupsOptionIncludeDisabled include disabled User Groups (default: false)
func GetUserGroupsOptionIncludeDisabled(b bool) GetUserGroupsOption {
return func(params *GetUserGroupsParams) {
params.IncludeDisabled = b
}
}
// GetUserGroupsOptionIncludeUsers include the list of users for each User Group (default: false)
func GetUserGroupsOptionIncludeUsers(b bool) GetUserGroupsOption {
return func(params *GetUserGroupsParams) {
params.IncludeUsers = b
}
}
// GetUserGroupsParams contains arguments for GetUserGroups method call
type GetUserGroupsParams struct {
IncludeCount bool
IncludeDisabled bool
IncludeUsers bool
}
// GetUserGroups returns a list of user groups for the team
func (api *Client) GetUserGroups(options ...GetUserGroupsOption) ([]UserGroup, error) {
return api.GetUserGroupsContext(context.Background(), options...)
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, options ...GetUserGroupsOption) ([]UserGroup, error) {
params := GetUserGroupsParams{}
for _, opt := range options {
opt(&params)
}
func (api *Client) GetUserGroupsContext(ctx context.Context) ([]UserGroup, error) {
values := url.Values{
"token": {api.token},
}
if params.IncludeCount {
values.Add("include_count", "true")
}
if params.IncludeDisabled {
values.Add("include_disabled", "true")
}
if params.IncludeUsers {
values.Add("include_users", "true")
}
response, err := api.userGroupRequest(ctx, "usergroups.list", values)
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.list", values, api.debug)
if err != nil {
return nil, err
}
@ -207,11 +163,7 @@ func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGro
values["description"] = []string{userGroup.Description}
}
if len(userGroup.Prefs.Channels) > 0 {
values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")}
}
response, err := api.userGroupRequest(ctx, "usergroups.update", values)
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.update", values, api.debug)
if err != nil {
return UserGroup{}, err
}
@ -230,7 +182,7 @@ func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup str
"usergroup": {userGroup},
}
response, err := api.userGroupRequest(ctx, "usergroups.users.list", values)
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.list", values, api.debug)
if err != nil {
return []string{}, err
}
@ -250,7 +202,7 @@ func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup
"users": {members},
}
response, err := api.userGroupRequest(ctx, "usergroups.users.update", values)
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.update", values, api.debug)
if err != nil {
return UserGroup{}, err
}

View File

@ -3,15 +3,16 @@ package slack
import (
"context"
"encoding/json"
"errors"
"net/url"
"strconv"
"time"
)
const (
DEFAULT_USER_PHOTO_CROP_X = -1
DEFAULT_USER_PHOTO_CROP_Y = -1
DEFAULT_USER_PHOTO_CROP_W = -1
errPaginationComplete = errorString("pagination complete")
)
// UserProfile contains all the information details of a given user
@ -36,7 +37,6 @@ type UserProfile struct {
ApiAppID string `json:"api_app_id,omitempty"`
StatusText string `json:"status_text,omitempty"`
StatusEmoji string `json:"status_emoji,omitempty"`
StatusExpiration int `json:"status_expiration"`
Team string `json:"team"`
Fields UserProfileCustomFields `json:"fields"`
}
@ -44,14 +44,14 @@ type UserProfile struct {
// UserProfileCustomFields represents user profile's custom fields.
// Slack API's response data type is inconsistent so we use the struct.
// For detail, please see below.
// https://github.com/slack-go/slack/pull/298#discussion_r185159233
// https://github.com/nlopes/slack/pull/298#discussion_r185159233
type UserProfileCustomFields struct {
fields map[string]UserProfileCustomField
}
// UnmarshalJSON is the implementation of the json.Unmarshaler interface.
func (fields *UserProfileCustomFields) UnmarshalJSON(b []byte) error {
// https://github.com/slack-go/slack/pull/298#discussion_r185159233
// https://github.com/nlopes/slack/pull/298#discussion_r185159233
if string(b) == "[]" {
return nil
}
@ -100,31 +100,28 @@ type UserProfileCustomField struct {
// User contains all the information of a user
type User struct {
ID string `json:"id"`
TeamID string `json:"team_id"`
Name string `json:"name"`
Deleted bool `json:"deleted"`
Color string `json:"color"`
RealName string `json:"real_name"`
TZ string `json:"tz,omitempty"`
TZLabel string `json:"tz_label"`
TZOffset int `json:"tz_offset"`
Profile UserProfile `json:"profile"`
IsBot bool `json:"is_bot"`
IsAdmin bool `json:"is_admin"`
IsOwner bool `json:"is_owner"`
IsPrimaryOwner bool `json:"is_primary_owner"`
IsRestricted bool `json:"is_restricted"`
IsUltraRestricted bool `json:"is_ultra_restricted"`
IsStranger bool `json:"is_stranger"`
IsAppUser bool `json:"is_app_user"`
IsInvitedUser bool `json:"is_invited_user"`
Has2FA bool `json:"has_2fa"`
HasFiles bool `json:"has_files"`
Presence string `json:"presence"`
Locale string `json:"locale"`
Updated JSONTime `json:"updated"`
Enterprise EnterpriseUser `json:"enterprise_user,omitempty"`
ID string `json:"id"`
TeamID string `json:"team_id"`
Name string `json:"name"`
Deleted bool `json:"deleted"`
Color string `json:"color"`
RealName string `json:"real_name"`
TZ string `json:"tz,omitempty"`
TZLabel string `json:"tz_label"`
TZOffset int `json:"tz_offset"`
Profile UserProfile `json:"profile"`
IsBot bool `json:"is_bot"`
IsAdmin bool `json:"is_admin"`
IsOwner bool `json:"is_owner"`
IsPrimaryOwner bool `json:"is_primary_owner"`
IsRestricted bool `json:"is_restricted"`
IsUltraRestricted bool `json:"is_ultra_restricted"`
IsStranger bool `json:"is_stranger"`
IsAppUser bool `json:"is_app_user"`
Has2FA bool `json:"has_2fa"`
HasFiles bool `json:"has_files"`
Presence string `json:"presence"`
Locale string `json:"locale"`
}
// UserPresence contains details about a user online status
@ -155,17 +152,6 @@ type UserIdentity struct {
Image512 string `json:"image_512"`
}
// EnterpriseUser is present when a user is part of Slack Enterprise Grid
// https://api.slack.com/types/user#enterprise_grid_user_objects
type EnterpriseUser struct {
ID string `json:"id"`
EnterpriseID string `json:"enterprise_id"`
EnterpriseName string `json:"enterprise_name"`
IsAdmin bool `json:"is_admin"`
IsOwner bool `json:"is_owner"`
Teams []string `json:"teams"`
}
type TeamIdentity struct {
ID string `json:"id"`
Name string `json:"name"`
@ -203,14 +189,16 @@ func NewUserSetPhotoParams() UserSetPhotoParams {
}
}
func (api *Client) userRequest(ctx context.Context, path string, values url.Values) (*userResponseFull, error) {
func userRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userResponseFull, error) {
response := &userResponseFull{}
err := api.postMethod(ctx, path, values, response)
err := postForm(ctx, client, SLACK_API+path, values, response, debug)
if err != nil {
return nil, err
}
return response, response.Err()
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// GetUserPresence will retrieve the current presence status of given user.
@ -225,7 +213,7 @@ func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*Us
"user": {user},
}
response, err := api.userRequest(ctx, "users.getPresence", values)
response, err := userRequest(ctx, api.httpclient, "users.getPresence", values, api.debug)
if err != nil {
return nil, err
}
@ -240,12 +228,11 @@ func (api *Client) GetUserInfo(user string) (*User, error) {
// GetUserInfoContext will retrieve the complete user information with a custom context
func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, error) {
values := url.Values{
"token": {api.token},
"user": {user},
"include_locale": {strconv.FormatBool(true)},
"token": {api.token},
"user": {user},
}
response, err := api.userRequest(ctx, "users.info", values)
response, err := userRequest(ctx, api.httpclient, "users.info", values, api.debug)
if err != nil {
return nil, err
}
@ -317,14 +304,13 @@ func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error)
t.previousResp = t.previousResp.initialize()
values := url.Values{
"limit": {strconv.Itoa(t.limit)},
"presence": {strconv.FormatBool(t.presence)},
"token": {t.c.token},
"cursor": {t.previousResp.Cursor},
"include_locale": {strconv.FormatBool(true)},
"limit": {strconv.Itoa(t.limit)},
"presence": {strconv.FormatBool(t.presence)},
"token": {t.c.token},
"cursor": {t.previousResp.Cursor},
}
if resp, err = t.c.userRequest(ctx, "users.list", values); err != nil {
if resp, err = userRequest(ctx, t.c.httpclient, "users.list", values, t.c.debug); err != nil {
return t, err
}
@ -347,19 +333,12 @@ func (api *Client) GetUsers() ([]User, error) {
// GetUsersContext returns the list of users (with their detailed information) with a custom context
func (api *Client) GetUsersContext(ctx context.Context) (results []User, err error) {
p := api.GetUsersPaginated()
for err == nil {
p, err = p.Next(ctx)
if err == nil {
results = append(results, p.Users...)
} else if rateLimitedError, ok := err.(*RateLimitedError); ok {
select {
case <-ctx.Done():
err = ctx.Err()
case <-time.After(rateLimitedError.RetryAfter):
err = nil
}
}
var (
p UserPagination
)
for p = api.GetUsersPaginated(); !p.Done(err); p, err = p.Next(ctx) {
results = append(results, p.Users...)
}
return results, p.Failure(err)
@ -376,7 +355,7 @@ func (api *Client) GetUserByEmailContext(ctx context.Context, email string) (*Us
"token": {api.token},
"email": {email},
}
response, err := api.userRequest(ctx, "users.lookupByEmail", values)
response, err := userRequest(ctx, api.httpclient, "users.lookupByEmail", values, api.debug)
if err != nil {
return nil, err
}
@ -394,7 +373,7 @@ func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) {
"token": {api.token},
}
_, err = api.userRequest(ctx, "users.setActive", values)
_, err = userRequest(ctx, api.httpclient, "users.setActive", values, api.debug)
return err
}
@ -410,7 +389,7 @@ func (api *Client) SetUserPresenceContext(ctx context.Context, presence string)
"presence": {presence},
}
_, err := api.userRequest(ctx, "users.setPresence", values)
_, err := userRequest(ctx, api.httpclient, "users.setPresence", values, api.debug)
return err
}
@ -420,21 +399,19 @@ func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) {
}
// GetUserIdentityContext will retrieve user info available per identity scopes with a custom context
func (api *Client) GetUserIdentityContext(ctx context.Context) (response *UserIdentityResponse, err error) {
func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityResponse, error) {
values := url.Values{
"token": {api.token},
}
response = &UserIdentityResponse{}
response := &UserIdentityResponse{}
err = api.postMethod(ctx, "users.identity", values, response)
err := postForm(ctx, api.httpclient, SLACK_API+"users.identity", values, response, api.debug)
if err != nil {
return nil, err
}
if err := response.Err(); err != nil {
return nil, err
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
@ -444,7 +421,7 @@ func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error {
}
// SetUserPhotoContext changes the currently authenticated user's profile image using a custom context
func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) (err error) {
func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) error {
response := &SlackResponse{}
values := url.Values{
"token": {api.token},
@ -459,7 +436,7 @@ func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params
values.Add("crop_w", strconv.Itoa(params.CropW))
}
err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", values, response, api)
err := postLocalWithMultipartResponse(ctx, api.httpclient, "users.setPhoto", image, "image", values, response, api.debug)
if err != nil {
return err
}
@ -473,13 +450,13 @@ func (api *Client) DeleteUserPhoto() error {
}
// DeleteUserPhotoContext deletes the current authenticated user's profile image with a custom context
func (api *Client) DeleteUserPhotoContext(ctx context.Context) (err error) {
func (api *Client) DeleteUserPhotoContext(ctx context.Context) error {
response := &SlackResponse{}
values := url.Values{
"token": {api.token},
}
err = api.postMethod(ctx, "users.deletePhoto", values, response)
err := postForm(ctx, api.httpclient, SLACK_API+"users.deletePhoto", values, response, api.debug)
if err != nil {
return err
}
@ -490,30 +467,15 @@ func (api *Client) DeleteUserPhotoContext(ctx context.Context) (err error) {
// 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. If statusExpiration is set to 0
// the status will not expire.
func (api *Client) SetUserCustomStatus(statusText, statusEmoji string, statusExpiration int64) error {
return api.SetUserCustomStatusContextWithUser(context.Background(), "", statusText, statusEmoji, statusExpiration)
// 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, statusExpiration int64) error {
return api.SetUserCustomStatusContextWithUser(context.Background(), "", statusText, statusEmoji, statusExpiration)
}
// SetUserCustomStatusWithUser will set a custom status and emoji for the provided user.
//
// For more information see SetUserCustomStatus
func (api *Client) SetUserCustomStatusWithUser(user, statusText, statusEmoji string, statusExpiration int64) error {
return api.SetUserCustomStatusContextWithUser(context.Background(), user, statusText, statusEmoji, statusExpiration)
}
// SetUserCustomStatusContextWithUser will set a custom status and emoji for the provided user with a custom context
//
// For more information see SetUserCustomStatus
func (api *Client) SetUserCustomStatusContextWithUser(ctx context.Context, user, statusText, statusEmoji string, statusExpiration int64) error {
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.
@ -526,13 +488,11 @@ func (api *Client) SetUserCustomStatusContextWithUser(ctx context.Context, user,
// - 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"`
StatusExpiration int64 `json:"status_expiration"`
StatusText string `json:"status_text"`
StatusEmoji string `json:"status_emoji"`
}{
StatusText: statusText,
StatusEmoji: statusEmoji,
StatusExpiration: statusExpiration,
StatusText: statusText,
StatusEmoji: statusEmoji,
},
)
@ -541,17 +501,20 @@ func (api *Client) SetUserCustomStatusContextWithUser(ctx context.Context, user,
}
values := url.Values{
"user": {user},
"token": {api.token},
"profile": {string(profile)},
}
response := &userResponseFull{}
if err = api.postMethod(ctx, "users.profile.set", values, response); err != nil {
if err = postForm(ctx, api.httpclient, SLACK_API+"users.profile.set", values, response, api.debug); err != nil {
return err
}
return response.Err()
if !response.Ok {
return errors.New(response.Error)
}
return nil
}
// UnsetUserCustomStatus removes the custom status message for the currently
@ -563,7 +526,7 @@ func (api *Client) UnsetUserCustomStatus() error {
// 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, "", "", 0)
return api.SetUserCustomStatusContext(ctx, "", "")
}
// GetUserProfile retrieves a user's profile information.
@ -584,14 +547,12 @@ func (api *Client) GetUserProfileContext(ctx context.Context, userID string, inc
}
resp := &getUserProfileResponse{}
err := api.postMethod(ctx, "users.profile.get", values, &resp)
err := postSlackMethod(ctx, api.httpclient, "users.profile.get", values, &resp, api.debug)
if err != nil {
return nil, err
}
if err := resp.Err(); err != nil {
return nil, err
if !resp.Ok {
return nil, errors.New(resp.Error)
}
return resp.Profile, nil
}

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

@ -0,0 +1,33 @@
package slack
import (
"github.com/pkg/errors"
"net/http"
"bytes"
"encoding/json"
)
type WebhookMessage struct {
Text string `json:"text,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
}
func PostWebhook(url string, msg *WebhookMessage) error {
raw, err := json.Marshal(msg)
if err != nil {
return errors.Wrap(err, "marshal failed")
}
response, err := http.Post(url, "application/json", bytes.NewReader(raw));
if err != nil {
return errors.Wrap(err, "failed to post webhook")
}
if response.StatusCode != http.StatusOK {
return statusCodeError{Code: response.StatusCode, Status: response.Status}
}
return nil
}

View File

@ -1,7 +1,8 @@
package slack
import (
"net/url"
"encoding/json"
"errors"
"sync"
"time"
@ -19,9 +20,6 @@ const (
//
// Create this element with Client's NewRTM() or NewRTMWithOptions(*RTMOptions)
type RTM struct {
// Client is the main API, embedded
Client
idGen IDGenerator
pingInterval time.Duration
pingDeadman *time.Timer
@ -31,9 +29,15 @@ type RTM struct {
IncomingEvents chan RTMEvent
outgoingMessages chan OutgoingMessage
killChannel chan bool
disconnected chan struct{}
disconnectedm *sync.Once
disconnected chan struct{} // disconnected is closed when Disconnect is invoked, regardless of connection state. Allows for ManagedConnection to not leak.
forcePing chan bool
rawEvents chan json.RawMessage
wasIntentional bool
isConnected bool
// Client is the main API, embedded
Client
websocketURL string
// UserDetails upon connection
info *Info
@ -49,35 +53,46 @@ type RTM struct {
// mu is mutex used to prevent RTM connection race conditions
mu *sync.Mutex
// connParams is a map of flags for connection parameters.
connParams url.Values
}
// signal that we are disconnected by closing the channel.
// protect it with a mutex to ensure it only happens once.
func (rtm *RTM) disconnect() {
rtm.disconnectedm.Do(func() {
close(rtm.disconnected)
})
// RTMOptions allows configuration of various options available for RTM messaging
//
// This structure will evolve in time so please make sure you are always using the
// named keys for every entry available as per Go 1 compatibility promise adding fields
// to this structure should not be considered a breaking change.
type RTMOptions struct {
// UseRTMStart set to true in order to use rtm.start or false to use rtm.connect
// As of 11th July 2017 you should prefer setting this to false, see:
// https://api.slack.com/changelog/2017-04-start-using-rtm-connect-and-stop-using-rtm-start
UseRTMStart bool
}
// Disconnect and wait, blocking until a successful disconnection.
func (rtm *RTM) Disconnect() error {
// always push into the kill channel when invoked,
// avoid RTM disconnect race conditions
rtm.mu.Lock()
defer rtm.mu.Unlock()
// always push into the disconnected channel when invoked,
// this lets the ManagedConnection() function properly clean up.
// if the buffer is full then just continue on.
select {
case rtm.killChannel <- true:
return nil
case <-rtm.disconnected:
return ErrAlreadyDisconnected
case rtm.disconnected <- struct{}{}:
default:
}
if !rtm.isConnected {
return errors.New("Invalid call to Disconnect - Slack API is already disconnected")
}
rtm.killChannel <- true
return nil
}
// GetInfo returns the info structure received when calling
// "startrtm", holding metadata needed to implement a full
// chat client. It will be non-nil after a call to StartRTM().
// "startrtm", holding all channels, groups and other metadata needed
// to implement a full chat client. It will be non-nil after a call to
// StartRTM().
func (rtm *RTM) GetInfo() *Info {
return rtm.info
}
@ -95,7 +110,7 @@ func (rtm *RTM) SendMessage(msg *OutgoingMessage) {
}
func (rtm *RTM) resetDeadman() {
rtm.pingDeadman.Reset(deadmanDuration(rtm.pingInterval))
timerReset(rtm.pingDeadman, deadmanDuration(rtm.pingInterval))
}
func deadmanDuration(d time.Duration) time.Duration {

Some files were not shown because too many files have changed in this diff Show More