4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-06-27 14:39:24 +00:00

Compare commits

...

32 Commits

Author SHA1 Message Date
Wim
6e410b096e Release v0.6.1 2016-09-17 15:34:59 +02:00
Wim
f9e5994348 Fix mattermost API change for UpdateLastViewedAt 2016-09-17 15:33:02 +02:00
Wim
ee77272cfd Release v0.6.0 2016-09-17 15:25:34 +02:00
Wim
16ed2aca6a Sync with mattermost 3.4.0 2016-09-17 15:19:18 +02:00
Wim
0f530e7902 Fix spinning for loop 2016-09-05 23:08:17 +02:00
Wim
4ed66ce20e Update documentation 2016-09-05 16:42:46 +02:00
Wim
b30e85836e Add Slack support 2016-09-05 16:34:37 +02:00
Wim
e449a97bd0 Release v0.6.0-beta2 2016-09-04 20:42:24 +02:00
Wim
39043f3fa4 Update documentation 2016-09-04 20:41:03 +02:00
Wim
12389d602e Add Gitter support 2016-09-04 20:04:43 +02:00
Wim
44144587a0 Get correct teamname for non-joined channels. Closes 42wim/matterircd#65 2016-09-01 00:15:48 +02:00
Wim
d0a30e354b Fix documentation layout 2016-08-20 18:16:22 +02:00
Wim
c261dc89d5 Fix documentation layout 2016-08-20 18:15:06 +02:00
Wim
c2c135bca2 Release v0.6.0-beta1 2016-08-20 18:09:00 +02:00
Wim
eb20cb237d Update documentation 2016-08-20 18:08:59 +02:00
Wim
106404d32f Fix info message 2016-08-20 18:08:59 +02:00
Wim
e06efbad9f Remove unused code 2016-08-20 18:08:58 +02:00
Wim
3311c7f923 Refactor handleReceive 2016-08-20 18:08:58 +02:00
Wim
3a6c655dfb Remove redundant function 2016-08-20 18:08:58 +02:00
Wim
e11d786775 Move nickformatting into bridge 2016-08-20 18:08:57 +02:00
Wim
889b6debc4 Add Connect() to Bridger interface 2016-08-20 18:08:57 +02:00
Wim
9cb3413d9c Add Enable per section (protocol) instead of in general section 2016-08-20 18:08:57 +02:00
Wim
131826e1d1 Fix crash on exit 2016-08-19 22:58:42 +02:00
Wim
96e21dd051 Add documentation about breaking API changes for mattermost 3.3.0. Start work on 0.6.0-dev 2016-08-15 21:11:50 +02:00
Wim
32e5f396e7 Make sure login works after logout 2016-08-15 20:27:36 +02:00
Wim
6c6000dbbd Update code to mattermost 3.3.0 API changes 2016-08-15 18:49:17 +02:00
Wim
24defcb970 Sync with mattermost 3.3.0 2016-08-15 18:47:31 +02:00
Wim
a1a11a88b3 Fix nil pointers 2016-08-14 23:04:28 +02:00
Wim
a997ae29ad Add StatusLoop(), keeps connection alive 2016-08-14 22:54:57 +02:00
Wim
ff94796700 Refactor bridge. Allows bridging between every protocol 2016-08-14 22:44:59 +02:00
Wim
1f72ca4c4e Add initial XMPP support 2016-08-14 22:40:26 +02:00
Wim
46faad8b57 Vendor go-xmpp 2016-08-14 22:40:25 +02:00
125 changed files with 11310 additions and 626 deletions

View File

@ -2,10 +2,10 @@ FROM alpine:edge
ENTRYPOINT ["/bin/matterbridge"]
COPY . /go/src/github.com/42wim/matterbridge
RUN apk update && apk add go git gcc musl-dev \
RUN apk update && apk add go git gcc musl-dev ca-certificates \
&& cd /go/src/github.com/42wim/matterbridge \
&& export GOPATH=/go \
&& go get \
&& go build -o /bin/matterbridge \
&& rm -rf /go \
&& apk del --purge git go
&& apk del --purge git go gcc musl-dev

View File

@ -1,25 +1,40 @@
# matterbridge
Simple bridge between mattermost and IRC.
Simple bridge between mattermost, IRC, XMPP and Gitter
* Relays public channel messages between mattermost and IRC.
* Supports multiple mattermost and irc channels.
* Relays public channel messages between mattermost, IRC, XMPP and Gitter. Pick and mix.
* Supports multiple channels.
* Matterbridge -plus also works with private groups on your mattermost.
This project has now [matterbridge-plus](https://github.com/42wim/matterbridge-plus/) merged in.
Breaking changes for matterbridge can be found in [migration](https://github.com/42wim/matterbridge/blob/master/migration.md)
Look at [matterbridge.conf.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.conf.sample) for documentation and an example.
## Changelog
Since v0.6.1 support for XMPP, Gitter and Slack is added. More details in [changelog.md] (https://github.com/42wim/matterbridge/blob/master/changelog.md)
## Requirements:
* [Mattermost] (https://github.com/mattermost/platform/) 3.x (stable, not a dev build)
### Webhooks version
* Configured incoming/outgoing [webhooks](https://www.mattermost.org/webhooks/) on your mattermost instance.
### Plus (API) version
* A dedicated user(bot) on your mattermost instance.
Accounts to one of the supported bridges
* [Mattermost] (https://github.com/mattermost/platform/)
* [IRC] (http://www.mirc.com/servers.html)
* [XMPP] (https://jabber.org)
* [Gitter] (https://gitter.im)
## binaries
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/tag/v0.5.0)
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/)
* For use with mattermost 3.3.0+ [v0.6.1](https://github.com/42wim/matterircd/releases/tag/v0.6.1)
* For use with mattermost 3.0.0-3.2.0 [v0.5.0](https://github.com/42wim/matterircd/releases/tag/v0.5.0)
## Compatibility
### Mattermost
* Matterbridge v0.6.1 works with mattermost 3.3.0 and higher [3.3.0 release](https://github.com/mattermost/platform/releases/tag/v3.3.0)
* Matterbridge v0.5.0 works with mattermost 3.0.0 - 3.2.0 [3.2.0 release](https://github.com/mattermost/platform/releases/tag/v3.2.0)
#### Webhooks version
* Configured incoming/outgoing [webhooks](https://www.mattermost.org/webhooks/) on your mattermost instance.
#### Plus (API) version
* A dedicated user(bot) on your mattermost instance.
## building
Go 1.6+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH] (https://golang.org/doc/code.html#GOPATH)
@ -48,7 +63,7 @@ Usage of ./matterbridge:
-debug
enable debug
-plus
running using API instead of webhooks
running using API instead of webhooks (deprecated, set Plus flag in [general] config)
-version
show version
```

View File

@ -1,407 +1,151 @@
package bridge
import (
"crypto/tls"
"github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook"
//"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/gitter"
"github.com/42wim/matterbridge/bridge/irc"
"github.com/42wim/matterbridge/bridge/mattermost"
"github.com/42wim/matterbridge/bridge/slack"
"github.com/42wim/matterbridge/bridge/xmpp"
log "github.com/Sirupsen/logrus"
"github.com/peterhellberg/giphy"
ircm "github.com/sorcix/irc"
"github.com/thoj/go-ircevent"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
//type Bridge struct {
type MMhook struct {
mh *matterhook.Client
}
type MMapi struct {
mc *matterclient.MMClient
mmMap map[string]string
mmIgnoreNicks []string
}
type MMirc struct {
i *irc.Connection
ircNick string
ircMap map[string]string
names map[string][]string
ircIgnoreNicks []string
}
type MMMessage struct {
Text string
Channel string
Username string
}
type Bridge struct {
MMhook
MMapi
MMirc
*Config
kind string
*config.Config
Source string
Bridges []Bridger
Channels []map[string]string
ignoreNicks map[string][]string
}
type FancyLog struct {
irc *log.Entry
mm *log.Entry
type Bridger interface {
Send(msg config.Message) error
Name() string
Connect() error
//Command(cmd string) string
}
var flog FancyLog
const Legacy = "legacy"
func initFLog() {
flog.irc = log.WithFields(log.Fields{"module": "irc"})
flog.mm = log.WithFields(log.Fields{"module": "mattermost"})
}
func NewBridge(name string, config *Config, kind string) *Bridge {
initFLog()
func NewBridge(cfg *config.Config) error {
c := make(chan config.Message)
b := &Bridge{}
b.Config = config
b.kind = kind
b.ircNick = b.Config.IRC.Nick
b.ircMap = make(map[string]string)
b.mmMap = make(map[string]string)
b.MMirc.names = make(map[string][]string)
b.ircIgnoreNicks = strings.Fields(b.Config.IRC.IgnoreNicks)
b.mmIgnoreNicks = strings.Fields(b.Config.Mattermost.IgnoreNicks)
for _, val := range b.Config.Channel {
b.ircMap[val.IRC] = val.Mattermost
b.mmMap[val.Mattermost] = val.IRC
b.Config = cfg
if cfg.IRC.Enable {
b.Bridges = append(b.Bridges, birc.New(cfg, c))
}
if kind == Legacy {
b.mh = matterhook.New(b.Config.Mattermost.URL,
matterhook.Config{InsecureSkipVerify: b.Config.Mattermost.SkipTLSVerify,
BindAddress: b.Config.Mattermost.BindAddress})
} else {
b.mc = matterclient.New(b.Config.Mattermost.Login, b.Config.Mattermost.Password,
b.Config.Mattermost.Team, b.Config.Mattermost.Server)
b.mc.SkipTLSVerify = b.Config.Mattermost.SkipTLSVerify
b.mc.NoTLS = b.Config.Mattermost.NoTLS
flog.mm.Infof("Trying login %s (team: %s) on %s", b.Config.Mattermost.Login, b.Config.Mattermost.Team, b.Config.Mattermost.Server)
err := b.mc.Login()
if err != nil {
flog.mm.Fatal("Can not connect", err)
}
flog.mm.Info("Login ok")
b.mc.JoinChannel(b.Config.Mattermost.Channel)
for _, val := range b.Config.Channel {
b.mc.JoinChannel(val.Mattermost)
}
go b.mc.WsReceiver()
if cfg.Mattermost.Enable {
b.Bridges = append(b.Bridges, bmattermost.New(cfg, c))
}
flog.irc.Info("Trying IRC connection")
b.i = b.createIRC(name)
flog.irc.Info("Connection succeeded")
go b.handleMatter()
return b
}
func (b *Bridge) createIRC(name string) *irc.Connection {
i := irc.IRC(b.Config.IRC.Nick, b.Config.IRC.Nick)
i.UseTLS = b.Config.IRC.UseTLS
i.UseSASL = b.Config.IRC.UseSASL
i.SASLLogin = b.Config.IRC.NickServNick
i.SASLPassword = b.Config.IRC.NickServPassword
i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.IRC.SkipTLSVerify}
if b.Config.IRC.Password != "" {
i.Password = b.Config.IRC.Password
if cfg.Xmpp.Enable {
b.Bridges = append(b.Bridges, bxmpp.New(cfg, c))
}
i.AddCallback(ircm.RPL_WELCOME, b.handleNewConnection)
err := i.Connect(b.Config.IRC.Server)
if err != nil {
flog.irc.Fatal(err)
if cfg.Gitter.Enable {
b.Bridges = append(b.Bridges, bgitter.New(cfg, c))
}
return i
}
func (b *Bridge) handleNewConnection(event *irc.Event) {
flog.irc.Info("Registering callbacks")
i := b.i
b.ircNick = event.Arguments[0]
i.AddCallback("PRIVMSG", b.handlePrivMsg)
i.AddCallback("CTCP_ACTION", b.handlePrivMsg)
i.AddCallback(ircm.RPL_ENDOFNAMES, b.endNames)
i.AddCallback(ircm.RPL_NAMREPLY, b.storeNames)
i.AddCallback(ircm.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
i.AddCallback(ircm.NOTICE, b.handleNotice)
i.AddCallback(ircm.RPL_MYINFO, func(e *irc.Event) { flog.irc.Infof("%s: %s", e.Code, strings.Join(e.Arguments[1:], " ")) })
i.AddCallback("PING", func(e *irc.Event) {
i.SendRaw("PONG :" + e.Message())
flog.irc.Debugf("PING/PONG")
})
if b.Config.Mattermost.ShowJoinPart {
i.AddCallback("JOIN", b.handleJoinPart)
i.AddCallback("PART", b.handleJoinPart)
if cfg.Slack.Enable {
b.Bridges = append(b.Bridges, bslack.New(cfg, c))
}
i.AddCallback("*", b.handleOther)
b.setupChannels()
}
func (b *Bridge) setupChannels() {
i := b.i
for _, val := range b.Config.Channel {
flog.irc.Infof("Joining %s as %s", val.IRC, b.ircNick)
i.Join(val.IRC)
if len(b.Bridges) < 2 {
log.Fatalf("only %d sections enabled. Need at least 2 sections enabled (eg [IRC] and [mattermost]", len(b.Bridges))
}
}
func (b *Bridge) handleIrcBotCommand(event *irc.Event) bool {
parts := strings.Fields(event.Message())
exp, _ := regexp.Compile("[:,]+$")
channel := event.Arguments[0]
command := ""
if len(parts) == 2 {
command = parts[1]
for _, br := range b.Bridges {
br.Connect()
}
if exp.ReplaceAllString(parts[0], "") == b.ircNick {
switch command {
case "users":
usernames := b.mc.UsernamesInChannel(b.getMMChannel(channel))
sort.Strings(usernames)
b.i.Privmsg(channel, "Users on Mattermost: "+strings.Join(usernames, ", "))
default:
b.i.Privmsg(channel, "Valid commands are: [users, help]")
}
return true
}
return false
}
func (b *Bridge) ircNickFormat(nick string) string {
if nick == b.ircNick {
return nick
}
if b.Config.Mattermost.RemoteNickFormat == nil {
return "irc-" + nick
}
return strings.Replace(*b.Config.Mattermost.RemoteNickFormat, "{NICK}", nick, -1)
}
func (b *Bridge) handlePrivMsg(event *irc.Event) {
flog.irc.Debugf("handlePrivMsg() %s %s", event.Nick, event.Message())
if b.ignoreMessage(event.Nick, event.Message(), "irc") {
return
}
if b.handleIrcBotCommand(event) {
return
}
msg := ""
if event.Code == "CTCP_ACTION" {
msg = event.Nick + " "
}
msg += event.Message()
b.Send(b.ircNickFormat(event.Nick), msg, b.getMMChannel(event.Arguments[0]))
}
func (b *Bridge) handleJoinPart(event *irc.Event) {
b.Send(b.ircNick, b.ircNickFormat(event.Nick)+" "+strings.ToLower(event.Code)+"s "+event.Message(), b.getMMChannel(event.Arguments[0]))
}
func (b *Bridge) handleNotice(event *irc.Event) {
if strings.Contains(event.Message(), "This nickname is registered") {
b.i.Privmsg(b.Config.IRC.NickServNick, "IDENTIFY "+b.Config.IRC.NickServPassword)
}
}
func (b *Bridge) nicksPerRow() int {
if b.Config.Mattermost.NicksPerRow < 1 {
return 4
}
return b.Config.Mattermost.NicksPerRow
}
func (b *Bridge) formatnicks(nicks []string, continued bool) string {
switch b.Config.Mattermost.NickFormatter {
case "table":
return tableformatter(nicks, b.nicksPerRow(), continued)
default:
return plainformatter(nicks, b.nicksPerRow())
}
}
func (b *Bridge) storeNames(event *irc.Event) {
channel := event.Arguments[2]
b.MMirc.names[channel] = append(
b.MMirc.names[channel],
strings.Split(strings.TrimSpace(event.Message()), " ")...)
}
func (b *Bridge) endNames(event *irc.Event) {
channel := event.Arguments[1]
sort.Strings(b.MMirc.names[channel])
maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
continued := false
for len(b.MMirc.names[channel]) > maxNamesPerPost {
b.Send(
b.ircNick,
b.formatnicks(b.MMirc.names[channel][0:maxNamesPerPost], continued),
b.getMMChannel(channel))
b.MMirc.names[channel] = b.MMirc.names[channel][maxNamesPerPost:]
continued = true
}
b.Send(b.ircNick, b.formatnicks(b.MMirc.names[channel], continued), b.getMMChannel(channel))
b.MMirc.names[channel] = nil
}
func (b *Bridge) handleTopicWhoTime(event *irc.Event) {
parts := strings.Split(event.Arguments[2], "!")
t, err := strconv.ParseInt(event.Arguments[3], 10, 64)
if err != nil {
flog.irc.Errorf("Invalid time stamp: %s", event.Arguments[3])
}
user := parts[0]
if len(parts) > 1 {
user += " [" + parts[1] + "]"
}
flog.irc.Infof("%s: Topic set by %s [%s]", event.Code, user, time.Unix(t, 0))
}
func (b *Bridge) handleOther(event *irc.Event) {
flog.irc.Debugf("%#v", event)
}
func (b *Bridge) Send(nick string, message string, channel string) error {
return b.SendType(nick, message, channel, "")
}
func (b *Bridge) SendType(nick string, message string, channel string, mtype string) error {
if b.Config.Mattermost.PrefixMessagesWithNick {
if IsMarkup(message) {
message = nick + "\n\n" + message
} else {
message = nick + " " + message
}
}
if b.kind == Legacy {
matterMessage := matterhook.OMessage{IconURL: b.Config.Mattermost.IconURL}
matterMessage.Channel = channel
matterMessage.UserName = nick
matterMessage.Type = mtype
matterMessage.Text = message
err := b.mh.Send(matterMessage)
if err != nil {
flog.mm.Info(err)
return err
}
flog.mm.Debug("->mattermost channel: ", channel, " ", message)
return nil
}
flog.mm.Debug("->mattermost channel: ", channel, " ", message)
b.mc.PostMessage(channel, message)
b.mapChannels()
b.mapIgnores()
b.handleReceive(c)
return nil
}
func (b *Bridge) handleMatterHook(mchan chan *MMMessage) {
func (b *Bridge) handleReceive(c chan config.Message) {
for {
message := b.mh.Receive()
flog.mm.Debugf("receiving from matterhook %#v", message)
m := &MMMessage{}
m.Username = message.UserName
m.Text = message.Text
m.Channel = message.ChannelName
mchan <- m
}
}
func (b *Bridge) handleMatterClient(mchan chan *MMMessage) {
for message := range b.mc.MessageChan {
// do not post our own messages back to irc
if message.Raw.Action == "posted" && b.mc.User.Username != message.Username {
flog.mm.Debugf("receiving from matterclient %#v", message)
m := &MMMessage{}
m.Username = message.Username
m.Channel = message.Channel
m.Text = message.Text
mchan <- m
select {
case msg := <-c:
for _, br := range b.Bridges {
b.handleMessage(msg, br)
}
}
}
}
func (b *Bridge) handleMatter() {
flog.mm.Infof("Choosing Mattermost connection type %s", b.kind)
mchan := make(chan *MMMessage)
if b.kind == Legacy {
go b.handleMatterHook(mchan)
} else {
go b.handleMatterClient(mchan)
func (b *Bridge) mapChannels() error {
for _, val := range b.Config.Channel {
m := make(map[string]string)
m["irc"] = val.IRC
m["mattermost"] = val.Mattermost
m["xmpp"] = val.Xmpp
m["gitter"] = val.Gitter
m["slack"] = val.Slack
b.Channels = append(b.Channels, m)
}
flog.mm.Info("Start listening for Mattermost messages")
for message := range mchan {
var username string
if b.ignoreMessage(message.Username, message.Text, "mattermost") {
continue
return nil
}
func (b *Bridge) mapIgnores() {
m := make(map[string][]string)
m["irc"] = strings.Fields(b.Config.IRC.IgnoreNicks)
m["mattermost"] = strings.Fields(b.Config.Mattermost.IgnoreNicks)
m["xmpp"] = strings.Fields(b.Config.Xmpp.IgnoreNicks)
m["gitter"] = strings.Fields(b.Config.Gitter.IgnoreNicks)
m["slack"] = strings.Fields(b.Config.Slack.IgnoreNicks)
b.ignoreNicks = m
}
func (b *Bridge) getDestChannel(msg *config.Message, dest string) string {
for _, v := range b.Channels {
if v[msg.Origin] == msg.Channel {
return v[dest]
}
username = message.Username + ": "
if b.Config.IRC.RemoteNickFormat != "" {
username = strings.Replace(b.Config.IRC.RemoteNickFormat, "{NICK}", message.Username, -1)
}
cmds := strings.Fields(message.Text)
// empty message
if len(cmds) == 0 {
continue
}
cmd := cmds[0]
switch cmd {
case "!users":
flog.mm.Info("Received !users from ", message.Username)
b.i.SendRaw("NAMES " + b.getIRCChannel(message.Channel))
continue
case "!gif":
message.Text = b.giphyRandom(strings.Fields(strings.Replace(message.Text, "!gif ", "", 1)))
b.Send(b.ircNick, message.Text, b.getIRCChannel(message.Channel))
continue
}
texts := strings.Split(message.Text, "\n")
for _, text := range texts {
flog.mm.Debug("Sending message from " + message.Username + " to " + message.Channel)
b.i.Privmsg(b.getIRCChannel(message.Channel), username+text)
}
return ""
}
func (b *Bridge) handleMessage(msg config.Message, dest Bridger) {
if b.ignoreMessage(&msg) {
return
}
if dest.Name() != msg.Origin {
msg.Channel = b.getDestChannel(&msg, dest.Name())
if msg.Channel == "" {
return
}
b.modifyMessage(&msg, dest.Name())
log.Debugf("sending %#v from %s to %s", msg, msg.Origin, dest.Name())
dest.Send(msg)
}
}
func (b *Bridge) giphyRandom(query []string) string {
g := giphy.DefaultClient
if b.Config.General.GiphyAPIKey != "" {
g.APIKey = b.Config.General.GiphyAPIKey
}
res, err := g.Random(query)
if err != nil {
return "error"
}
return res.Data.FixedHeightDownsampledURL
}
func (b *Bridge) getMMChannel(ircChannel string) string {
mmChannel := b.ircMap[ircChannel]
if b.kind == Legacy {
return mmChannel
}
return b.mc.GetChannelId(mmChannel, "")
}
func (b *Bridge) getIRCChannel(mmChannel string) string {
return b.mmMap[mmChannel]
}
func (b *Bridge) ignoreMessage(nick string, message string, protocol string) bool {
var ignoreNicks = b.mmIgnoreNicks
if protocol == "irc" {
ignoreNicks = b.ircIgnoreNicks
}
func (b *Bridge) ignoreMessage(msg *config.Message) bool {
// should we discard messages ?
for _, entry := range ignoreNicks {
if nick == entry {
for _, entry := range b.ignoreNicks[msg.Origin] {
if msg.Username == entry {
return true
}
}
return false
}
func setNickFormat(msg *config.Message, format string) {
if format == "" {
msg.Username = msg.Origin + "-" + msg.Username + ": "
return
}
msg.Username = strings.Replace(format, "{NICK}", msg.Username, -1)
msg.Username = strings.Replace(msg.Username, "{BRIDGE}", msg.Origin, -1)
}
func (b *Bridge) modifyMessage(msg *config.Message, dest string) {
switch dest {
case "irc":
setNickFormat(msg, b.Config.IRC.RemoteNickFormat)
case "gitter":
setNickFormat(msg, b.Config.Gitter.RemoteNickFormat)
case "xmpp":
setNickFormat(msg, b.Config.Xmpp.RemoteNickFormat)
case "mattermost":
setNickFormat(msg, b.Config.Mattermost.RemoteNickFormat)
case "slack":
setNickFormat(msg, b.Config.Slack.RemoteNickFormat)
}
}

View File

@ -1,4 +1,4 @@
package bridge
package config
import (
"gopkg.in/gcfg.v1"
@ -6,6 +6,13 @@ import (
"log"
)
type Message struct {
Text string
Channel string
Username string
Origin string
}
type Config struct {
IRC struct {
UseTLS bool
@ -19,6 +26,14 @@ type Config struct {
NickServPassword string
RemoteNickFormat string
IgnoreNicks string
Enable bool
}
Gitter struct {
Enable bool
IgnoreNicks string
Nick string
RemoteNickFormat string
Token string
}
Mattermost struct {
URL string
@ -34,16 +49,47 @@ type Config struct {
Team string
Login string
Password string
RemoteNickFormat *string
RemoteNickFormat string
IgnoreNicks string
NoTLS bool
Enable bool
}
Slack struct {
BindAddress string
Enable bool
IconURL string
IgnoreNicks string
NickFormatter string
NicksPerRow int
PrefixMessagesWithNick bool
RemoteNickFormat string
Token string
URL string
UseAPI bool
}
Xmpp struct {
IgnoreNicks string
Jid string
Password string
Server string
Muc string
Nick string
RemoteNickFormat string
Enable bool
}
Channel map[string]*struct {
IRC string
Mattermost string
Xmpp string
Gitter string
Slack string
}
General struct {
GiphyAPIKey string
Xmpp bool
Irc bool
Mattermost bool
Plus bool
}
}

110
bridge/gitter/gitter.go Normal file
View File

@ -0,0 +1,110 @@
package bgitter
import (
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/sromku/go-gitter"
"strings"
)
type Bgitter struct {
c *gitter.Gitter
*config.Config
Remote chan config.Message
Rooms []gitter.Room
}
type Message struct {
Text string
Channel string
Username string
}
var flog *log.Entry
func init() {
flog = log.WithFields(log.Fields{"module": "gitter"})
}
func New(config *config.Config, c chan config.Message) *Bgitter {
b := &Bgitter{}
b.Config = config
b.Remote = c
return b
}
func (b *Bgitter) Connect() error {
var err error
flog.Info("Trying Gitter connection")
b.c = gitter.New(b.Config.Gitter.Token)
_, err = b.c.GetUser()
if err != nil {
flog.Debugf("%#v", err)
return err
}
flog.Info("Connection succeeded")
b.setupChannels()
go b.handleGitter()
return nil
}
func (b *Bgitter) Name() string {
return "gitter"
}
func (b *Bgitter) Send(msg config.Message) error {
roomID := b.getRoomID(msg.Channel)
if roomID == "" {
flog.Errorf("Could not find roomID for %v", msg.Channel)
return nil
}
// add ZWSP because gitter echoes our own messages
return b.c.SendMessage(roomID, msg.Username+msg.Text+" ")
}
func (b *Bgitter) getRoomID(channel string) string {
for _, v := range b.Rooms {
if v.URI == channel {
return v.ID
}
}
return ""
}
func (b *Bgitter) handleGitter() {
for _, val := range b.Config.Channel {
room := val.Gitter
roomID := b.getRoomID(room)
if roomID == "" {
continue
}
stream := b.c.Stream(roomID)
go b.c.Listen(stream)
go func(stream *gitter.Stream, room string) {
for {
event := <-stream.Event
switch ev := event.Data.(type) {
case *gitter.MessageReceived:
// check for ZWSP to see if it's not an echo
if !strings.HasSuffix(ev.Message.Text, "") {
b.Remote <- config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room, Origin: "gitter"}
}
case *gitter.GitterConnectionClosed:
flog.Errorf("connection with gitter closed for room %s", room)
}
}
}(stream, room)
}
}
func (b *Bgitter) setupChannels() {
b.Rooms, _ = b.c.GetRooms()
for _, val := range b.Config.Channel {
flog.Infof("Joining %s as %s", val.Gitter, b.Gitter.Nick)
_, err := b.c.JoinRoom(val.Gitter)
if err != nil {
log.Errorf("Joining %s failed", val.Gitter)
}
}
}

View File

@ -1,4 +1,4 @@
package bridge
package birc
import (
"strings"

194
bridge/irc/irc.go Normal file
View File

@ -0,0 +1,194 @@
package birc
import (
"crypto/tls"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
ircm "github.com/sorcix/irc"
"github.com/thoj/go-ircevent"
"sort"
"strconv"
"strings"
"time"
)
//type Bridge struct {
type Birc struct {
i *irc.Connection
ircNick string
ircMap map[string]string
names map[string][]string
ircIgnoreNicks []string
*config.Config
Remote chan config.Message
}
type FancyLog struct {
irc *log.Entry
}
var flog FancyLog
func init() {
flog.irc = log.WithFields(log.Fields{"module": "irc"})
}
func New(config *config.Config, c chan config.Message) *Birc {
b := &Birc{}
b.Config = config
b.Remote = c
b.ircNick = b.Config.IRC.Nick
b.ircMap = make(map[string]string)
b.names = make(map[string][]string)
b.ircIgnoreNicks = strings.Fields(b.Config.IRC.IgnoreNicks)
return b
}
func (b *Birc) Command(msg *config.Message) string {
switch msg.Text {
case "!users":
b.i.SendRaw("NAMES " + msg.Channel)
}
return ""
}
func (b *Birc) Connect() error {
flog.irc.Info("Trying IRC connection")
i := irc.IRC(b.Config.IRC.Nick, b.Config.IRC.Nick)
i.UseTLS = b.Config.IRC.UseTLS
i.UseSASL = b.Config.IRC.UseSASL
i.SASLLogin = b.Config.IRC.NickServNick
i.SASLPassword = b.Config.IRC.NickServPassword
i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.IRC.SkipTLSVerify}
if b.Config.IRC.Password != "" {
i.Password = b.Config.IRC.Password
}
i.AddCallback(ircm.RPL_WELCOME, b.handleNewConnection)
err := i.Connect(b.Config.IRC.Server)
if err != nil {
return err
}
flog.irc.Info("Connection succeeded")
b.i = i
return nil
}
func (b *Birc) Name() string {
return "irc"
}
func (b *Birc) Send(msg config.Message) error {
if msg.Origin == "irc" {
return nil
}
if strings.HasPrefix(msg.Text, "!") {
b.Command(&msg)
return nil
}
b.i.Privmsg(msg.Channel, msg.Username+msg.Text)
return nil
}
func (b *Birc) endNames(event *irc.Event) {
channel := event.Arguments[1]
sort.Strings(b.names[channel])
maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
continued := false
for len(b.names[channel]) > maxNamesPerPost {
b.Remote <- config.Message{Username: b.ircNick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost], continued), Channel: channel, Origin: "irc"}
b.names[channel] = b.names[channel][maxNamesPerPost:]
continued = true
}
b.Remote <- config.Message{Username: b.ircNick, Text: b.formatnicks(b.names[channel], continued), Channel: channel, Origin: "irc"}
b.names[channel] = nil
}
func (b *Birc) handleNewConnection(event *irc.Event) {
flog.irc.Info("Registering callbacks")
i := b.i
b.ircNick = event.Arguments[0]
i.AddCallback("PRIVMSG", b.handlePrivMsg)
i.AddCallback("CTCP_ACTION", b.handlePrivMsg)
i.AddCallback(ircm.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
i.AddCallback(ircm.RPL_ENDOFNAMES, b.endNames)
i.AddCallback(ircm.RPL_NAMREPLY, b.storeNames)
i.AddCallback(ircm.NOTICE, b.handleNotice)
i.AddCallback(ircm.RPL_MYINFO, func(e *irc.Event) { flog.irc.Infof("%s: %s", e.Code, strings.Join(e.Arguments[1:], " ")) })
i.AddCallback("PING", func(e *irc.Event) {
i.SendRaw("PONG :" + e.Message())
flog.irc.Debugf("PING/PONG")
})
if b.Config.Mattermost.ShowJoinPart {
i.AddCallback("JOIN", b.handleJoinPart)
i.AddCallback("PART", b.handleJoinPart)
}
i.AddCallback("*", b.handleOther)
b.setupChannels()
}
func (b *Birc) handleJoinPart(event *irc.Event) {
//b.Send(b.ircNick, b.ircNickFormat(event.Nick)+" "+strings.ToLower(event.Code)+"s "+event.Message(), b.getMMChannel(event.Arguments[0]))
}
func (b *Birc) handleNotice(event *irc.Event) {
if strings.Contains(event.Message(), "This nickname is registered") {
b.i.Privmsg(b.Config.IRC.NickServNick, "IDENTIFY "+b.Config.IRC.NickServPassword)
}
}
func (b *Birc) handleOther(event *irc.Event) {
flog.irc.Debugf("%#v", event)
}
func (b *Birc) handlePrivMsg(event *irc.Event) {
flog.irc.Debugf("handlePrivMsg() %s %s", event.Nick, event.Message())
msg := ""
if event.Code == "CTCP_ACTION" {
msg = event.Nick + " "
}
msg += event.Message()
b.Remote <- config.Message{Username: event.Nick, Text: msg, Channel: event.Arguments[0], Origin: "irc"}
}
func (b *Birc) handleTopicWhoTime(event *irc.Event) {
parts := strings.Split(event.Arguments[2], "!")
t, err := strconv.ParseInt(event.Arguments[3], 10, 64)
if err != nil {
flog.irc.Errorf("Invalid time stamp: %s", event.Arguments[3])
}
user := parts[0]
if len(parts) > 1 {
user += " [" + parts[1] + "]"
}
flog.irc.Infof("%s: Topic set by %s [%s]", event.Code, user, time.Unix(t, 0))
}
func (b *Birc) nicksPerRow() int {
if b.Config.Mattermost.NicksPerRow < 1 {
return 4
}
return b.Config.Mattermost.NicksPerRow
}
func (b *Birc) setupChannels() {
for _, val := range b.Config.Channel {
flog.irc.Infof("Joining %s as %s", val.IRC, b.ircNick)
b.i.Join(val.IRC)
}
}
func (b *Birc) storeNames(event *irc.Event) {
channel := event.Arguments[2]
b.names[channel] = append(
b.names[channel],
strings.Split(strings.TrimSpace(event.Message()), " ")...)
}
func (b *Birc) formatnicks(nicks []string, continued bool) string {
switch b.Config.Mattermost.NickFormatter {
case "table":
return tableformatter(nicks, b.nicksPerRow(), continued)
default:
return plainformatter(nicks, b.nicksPerRow())
}
}

View File

@ -0,0 +1,174 @@
package bmattermost
import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus"
"strings"
)
//type Bridge struct {
type MMhook struct {
mh *matterhook.Client
}
type MMapi struct {
mc *matterclient.MMClient
mmMap map[string]string
mmIgnoreNicks []string
}
type MMMessage struct {
Text string
Channel string
Username string
}
type Bmattermost struct {
MMhook
MMapi
*config.Config
Plus bool
Remote chan config.Message
}
type FancyLog struct {
irc *log.Entry
mm *log.Entry
xmpp *log.Entry
}
var flog FancyLog
const Legacy = "legacy"
func init() {
flog.irc = log.WithFields(log.Fields{"module": "irc"})
flog.mm = log.WithFields(log.Fields{"module": "mattermost"})
flog.xmpp = log.WithFields(log.Fields{"module": "xmpp"})
}
func New(cfg *config.Config, c chan config.Message) *Bmattermost {
b := &Bmattermost{}
b.Config = cfg
b.Remote = c
b.Plus = cfg.General.Plus
b.mmMap = make(map[string]string)
return b
}
func (b *Bmattermost) Command(cmd string) string {
return ""
}
func (b *Bmattermost) Connect() error {
if !b.Plus {
b.mh = matterhook.New(b.Config.Mattermost.URL,
matterhook.Config{InsecureSkipVerify: b.Config.Mattermost.SkipTLSVerify,
BindAddress: b.Config.Mattermost.BindAddress})
} else {
b.mc = matterclient.New(b.Config.Mattermost.Login, b.Config.Mattermost.Password,
b.Config.Mattermost.Team, b.Config.Mattermost.Server)
b.mc.SkipTLSVerify = b.Config.Mattermost.SkipTLSVerify
b.mc.NoTLS = b.Config.Mattermost.NoTLS
flog.mm.Infof("Trying login %s (team: %s) on %s", b.Config.Mattermost.Login, b.Config.Mattermost.Team, b.Config.Mattermost.Server)
err := b.mc.Login()
if err != nil {
return err
}
flog.mm.Info("Login ok")
b.mc.JoinChannel(b.Config.Mattermost.Channel)
for _, val := range b.Config.Channel {
b.mc.JoinChannel(val.Mattermost)
}
go b.mc.WsReceiver()
}
go b.handleMatter()
return nil
}
func (b *Bmattermost) Name() string {
return "mattermost"
}
func (b *Bmattermost) Send(msg config.Message) error {
flog.mm.Infof("mattermost send %#v", msg)
if msg.Origin != "mattermost" {
return b.SendType(msg.Username, msg.Text, msg.Channel, "")
}
return nil
}
func (b *Bmattermost) SendType(nick string, message string, channel string, mtype string) error {
if b.Config.Mattermost.PrefixMessagesWithNick {
/*if IsMarkup(message) {
message = nick + "\n\n" + message
} else {
*/
message = nick + " " + message
//}
}
if !b.Plus {
matterMessage := matterhook.OMessage{IconURL: b.Config.Mattermost.IconURL}
matterMessage.Channel = channel
matterMessage.UserName = nick
matterMessage.Type = mtype
matterMessage.Text = message
err := b.mh.Send(matterMessage)
if err != nil {
flog.mm.Info(err)
return err
}
flog.mm.Debug("->mattermost channel: ", channel, " ", message)
return nil
}
flog.mm.Debug("->mattermost channel plus: ", channel, " ", message)
b.mc.PostMessage(b.mc.GetChannelId(channel, ""), message)
return nil
}
func (b *Bmattermost) handleMatter() {
flog.mm.Infof("Choosing API based Mattermost connection: %t", b.Plus)
mchan := make(chan *MMMessage)
if b.Plus {
go b.handleMatterClient(mchan)
} else {
go b.handleMatterHook(mchan)
}
flog.mm.Info("Start listening for Mattermost messages")
for message := range mchan {
texts := strings.Split(message.Text, "\n")
for _, text := range texts {
flog.mm.Debug("Sending message from " + message.Username + " to " + message.Channel)
b.Remote <- config.Message{Text: text, Username: message.Username, Channel: message.Channel, Origin: "mattermost"}
}
}
}
func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
for message := range b.mc.MessageChan {
// do not post our own messages back to irc
if message.Raw.Event == "posted" && b.mc.User.Username != message.Username {
flog.mm.Debugf("receiving from matterclient %#v", message)
flog.mm.Debugf("receiving from matterclient %#v", message.Raw)
m := &MMMessage{}
m.Username = message.Username
m.Channel = message.Channel
m.Text = message.Text
mchan <- m
}
}
}
func (b *Bmattermost) handleMatterHook(mchan chan *MMMessage) {
for {
message := b.mh.Receive()
flog.mm.Debugf("receiving from matterhook %#v", message)
m := &MMMessage{}
m.Username = message.UserName
m.Text = message.Text
m.Channel = message.ChannelName
mchan <- m
}
}

180
bridge/slack/slack.go Normal file
View File

@ -0,0 +1,180 @@
package bslack
import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus"
"github.com/nlopes/slack"
"strings"
"time"
)
type MMMessage struct {
Text string
Channel string
Username string
}
type bslack struct {
mh *matterhook.Client
sc *slack.Client
// MMapi
*config.Config
rtm *slack.RTM
Plus bool
Remote chan config.Message
channels []slack.Channel
}
var flog *log.Entry
func init() {
flog = log.WithFields(log.Fields{"module": "slack"})
}
func New(cfg *config.Config, c chan config.Message) *bslack {
b := &bslack{}
b.Config = cfg
b.Remote = c
b.Plus = cfg.Slack.UseAPI
return b
}
func (b *bslack) Command(cmd string) string {
return ""
}
func (b *bslack) Connect() error {
if !b.Plus {
b.mh = matterhook.New(b.Config.Slack.URL,
matterhook.Config{BindAddress: b.Config.Slack.BindAddress})
} else {
b.sc = slack.New(b.Config.Slack.Token)
flog.Infof("Trying login on slack with Token")
/*
if err != nil {
return err
}
*/
flog.Info("Login ok")
}
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
go b.handleSlack()
return nil
}
func (b *bslack) Name() string {
return "slack"
}
func (b *bslack) Send(msg config.Message) error {
flog.Infof("slack send %#v", msg)
if msg.Origin != "slack" {
return b.SendType(msg.Username, msg.Text, msg.Channel, "")
}
return nil
}
func (b *bslack) SendType(nick string, message string, channel string, mtype string) error {
if b.Config.Slack.PrefixMessagesWithNick {
message = nick + " " + message
}
if !b.Plus {
matterMessage := matterhook.OMessage{IconURL: b.Config.Slack.IconURL}
matterMessage.Channel = channel
matterMessage.UserName = nick
matterMessage.Type = mtype
matterMessage.Text = message
err := b.mh.Send(matterMessage)
if err != nil {
flog.Info(err)
return err
}
flog.Debug("->slack channel: ", channel, " ", message)
return nil
}
flog.Debugf("sent to slack channel API: %s %s", channel, message)
newmsg := b.rtm.NewOutgoingMessage(message, b.getChannelByName(channel).ID)
b.rtm.SendMessage(newmsg)
return nil
}
func (b *bslack) getChannelByName(name string) *slack.Channel {
if b.channels == nil {
return nil
}
for _, channel := range b.channels {
if channel.Name == name {
return &channel
}
}
return nil
}
func (b *bslack) handleSlack() {
flog.Infof("Choosing API based slack connection: %t", b.Plus)
mchan := make(chan *MMMessage)
if b.Plus {
go b.handleSlackClient(mchan)
} else {
go b.handleMatterHook(mchan)
}
time.Sleep(time.Second)
flog.Info("Start listening for Slack messages")
for message := range mchan {
texts := strings.Split(message.Text, "\n")
for _, text := range texts {
flog.Debug("Sending message from " + message.Username + " to " + message.Channel)
b.Remote <- config.Message{Text: text, Username: message.Username, Channel: message.Channel, Origin: "slack"}
}
}
}
func (b *bslack) handleSlackClient(mchan chan *MMMessage) {
for msg := range b.rtm.IncomingEvents {
switch ev := msg.Data.(type) {
case *slack.MessageEvent:
flog.Debugf("%#v", ev)
channel, err := b.rtm.GetChannelInfo(ev.Channel)
if err != nil {
continue
}
user, err := b.rtm.GetUserInfo(ev.User)
if err != nil {
continue
}
m := &MMMessage{}
m.Username = user.Name
m.Channel = channel.Name
m.Text = ev.Text
mchan <- m
case *slack.OutgoingErrorEvent:
flog.Debugf("%#v", ev.Error())
case *slack.ConnectedEvent:
b.channels = ev.Info.Channels
for _, val := range b.Config.Channel {
channel := b.getChannelByName(val.Slack)
if channel != nil && !channel.IsMember {
flog.Infof("Joining %s", val.Slack)
b.sc.JoinChannel(channel.ID)
}
}
case *slack.InvalidAuthEvent:
flog.Fatalf("Invalid Token %#v", ev)
default:
}
}
}
func (b *bslack) handleMatterHook(mchan chan *MMMessage) {
for {
message := b.mh.Receive()
flog.Debugf("receiving from slack %#v", message)
m := &MMMessage{}
m.Username = message.UserName
m.Text = message.Text
m.Channel = message.ChannelName
mchan <- m
}
}

133
bridge/xmpp/xmpp.go Normal file
View File

@ -0,0 +1,133 @@
package bxmpp
import (
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/mattn/go-xmpp"
"strings"
"time"
)
type Bxmpp struct {
xc *xmpp.Client
xmppMap map[string]string
*config.Config
Remote chan config.Message
}
type FancyLog struct {
xmpp *log.Entry
}
type Message struct {
Text string
Channel string
Username string
}
var flog FancyLog
func init() {
flog.xmpp = log.WithFields(log.Fields{"module": "xmpp"})
}
func New(config *config.Config, c chan config.Message) *Bxmpp {
b := &Bxmpp{}
b.xmppMap = make(map[string]string)
b.Config = config
b.Remote = c
return b
}
func (b *Bxmpp) Connect() error {
var err error
flog.xmpp.Info("Trying XMPP connection")
b.xc, err = b.createXMPP()
if err != nil {
flog.xmpp.Debugf("%#v", err)
return err
}
flog.xmpp.Info("Connection succeeded")
b.setupChannels()
go b.handleXmpp()
return nil
}
func (b *Bxmpp) Name() string {
return "xmpp"
}
func (b *Bxmpp) Send(msg config.Message) error {
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Xmpp.Muc, Text: msg.Username + msg.Text})
return nil
}
func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
options := xmpp.Options{
Host: b.Config.Xmpp.Server,
User: b.Config.Xmpp.Jid,
Password: b.Config.Xmpp.Password,
NoTLS: true,
StartTLS: true,
//StartTLS: false,
Debug: true,
Session: true,
Status: "",
StatusMessage: "",
Resource: "",
InsecureAllowUnencryptedAuth: false,
//InsecureAllowUnencryptedAuth: true,
}
var err error
b.xc, err = options.NewClient()
return b.xc, err
}
func (b *Bxmpp) setupChannels() {
for _, val := range b.Config.Channel {
flog.xmpp.Infof("Joining %s as %s", val.Xmpp, b.Xmpp.Nick)
b.xc.JoinMUCNoHistory(val.Xmpp+"@"+b.Xmpp.Muc, b.Xmpp.Nick)
}
}
func (b *Bxmpp) xmppKeepAlive() {
go func() {
ticker := time.NewTicker(90 * time.Second)
for {
select {
case <-ticker.C:
b.xc.Send(xmpp.Chat{})
}
}
}()
}
func (b *Bxmpp) handleXmpp() error {
for {
m, err := b.xc.Recv()
if err != nil {
return err
}
switch v := m.(type) {
case xmpp.Chat:
var channel, nick string
if v.Type == "groupchat" {
s := strings.Split(v.Remote, "@")
if len(s) == 2 {
channel = s[0]
}
s = strings.Split(s[1], "/")
if len(s) == 2 {
nick = s[1]
}
if nick != b.Xmpp.Nick {
flog.xmpp.Infof("sending message to remote %s %s %s", nick, v.Text, channel)
b.Remote <- config.Message{Username: nick, Text: v.Text, Channel: channel, Origin: "xmpp"}
}
}
case xmpp.Presence:
// do nothing
}
}
}

85
changelog.md Normal file
View File

@ -0,0 +1,85 @@
# v0.6.1
## New features
* Slack support added. See matterbridge.conf.sample for more information
## Bugfix
* Fix 100% CPU bug on incorrect closed connections
# v0.6.0-beta2
## New features
* Gitter support added. See matterbridge.conf.sample for more information
# v0.6.0-beta1
## Breaking changes from 0.5 to 0.6
### commandline
* -plus switch deprecated. Use ```Plus=true``` or ```Plus``` in ```[general]``` section
### IRC section
* ```Enabled``` added (default false)
Add ```Enabled=true``` or ```Enabled``` to the ```[IRC]``` section if you want to enable the IRC bridge
### Mattermost section
* ```Enabled``` added (default false)
Add ```Enabled=true``` or ```Enabled``` to the ```[mattermost]``` section if you want to enable the mattermost bridge
### General section
* Use ```Plus=true``` or ```Plus``` in ```[general]``` section to enable the API version of matterbridge
## New features
* Matterbridge now bridges between any specified protocol (not only mattermost anymore)
* XMPP support added. See matterbridge.conf.sample for more information
* RemoteNickFormat {BRIDGE} variable added
You can now add the originating bridge to ```RemoteNickFormat```
eg ```RemoteNickFormat="[{BRIDGE}] <{NICK}> "```
# v0.5.0
## Breaking changes from 0.4 to 0.5 for matterbridge (webhooks version)
### IRC section
#### Server
Port removed, added to server
```
server="irc.freenode.net"
port=6667
```
changed to
```
server="irc.freenode.net:6667"
```
#### Channel
Removed see Channels section below
#### UseSlackCircumfix=true
Removed, can be done by using ```RemoteNickFormat="<{NICK}> "```
### Mattermost section
#### BindAddress
Port removed, added to BindAddress
```
BindAddress="0.0.0.0"
port=9999
```
changed to
```
BindAddress="0.0.0.0:9999"
```
#### Token
Removed
### Channels section
```
[Token "outgoingwebhooktoken1"]
IRCChannel="#off-topic"
MMChannel="off-topic"
```
changed to
```
[Channel "channelnameofchoice"]
IRC="#off-topic"
Mattermost="off-topic"
```

View File

@ -3,6 +3,9 @@
#IRC section
###################################################################
[IRC]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#irc server to connect to.
#REQUIRED
Server="irc.freenode.net:6667"
@ -13,7 +16,7 @@ UseTLS=false
#Enable SASL (PLAIN) authentication. (freenode requires this from eg AWS hosts)
#It uses NickServNick and NickServPassword as login and password
#OPTIONAL (deefault false)
#OPTIONAL (default false)
UseSASL=false
#Enable to not verify the certificate on your irc server. i
@ -31,21 +34,55 @@ Nick="matterbot"
NickServNick="nickserv"
NickServPassword="secret"
#RemoteNickFormat defines how Mattermost users appear on irc
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#OPTIONAL (default NICK:)
RemoteNickFormat="{NICK}: "
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#OPTIONAL (default {BRIDGE}-{NICK})
RemoteNickFormat="[{BRIDGE}] <{NICK}>
#Nicks you want to ignore.
#Messages from those users will not be sent to mattermost.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
###################################################################
#XMPP section
###################################################################
[XMPP]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#xmpp server to connect to.
#REQUIRED
Server="jabber.example.com:5222"
#Jid
#REQUIRED
Jid="user@example.com"
#Password
#REQUIRED
Password="yourpass"
#MUC
#REQUIRED
Muc="conference.jabber.example.com"
#Your nick in the rooms
#REQUIRED
Nick="xmppbot"
###################################################################
#mattermost section
###################################################################
[mattermost]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#### Settings for webhook matterbridge.
#### These settings will not be used when using -plus switch which doesn't use
#### webhooks.
@ -83,7 +120,7 @@ Team="yourteam"
Login="yourlogin"
Password="yourpass"
#Disable to make a http connection to your mattermost.
#Enable this to make a http connection (instead of https) to your mattermost.
#OPTIONAL (default false)
NoTLS=false
@ -98,18 +135,19 @@ SkipTLSVerify=true
#OPTIONAL (default false)
ShowJoinPart=false
#Whether to prefix messages from IRC to mattermost with the sender's nick.
#Whether to prefix messages from other bridges to mattermost with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the
#mattermost server. If you set PrefixMessagesWithNick to true, each message
#from IRC to Mattermost will by default be prefixed by "irc-" + nick. You can,
#from bridge to Mattermost will by default be prefixed by "bridge-" + nick. You can,
#however, modify how the messages appear, by setting (and modifying) RemoteNickFormat
#OPTIONAL (default false)
PrefixMessagesWithNick=false
#RemoteNickFormat defines how IRC users appear on Mattermost.
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#OPTIONAL (default irc-NICK)
RemoteNickFormat="irc-{NICK}"
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#OPTIONAL (default {BRIDGE}-{NICK})
RemoteNickFormat="[{BRIDGE}] <{NICK}>
#how to format the list of IRC nicks when displayed in mattermost.
#Possible options are "table" and "plain"
@ -119,7 +157,95 @@ NickFormatter=plain
#OPTIONAL (default 4)
NicksPerRow=4
#Nicks you want to ignore. Messages from those users will not be sent to IRC.
#Nicks you want to ignore. Messages from those users will not be bridged.
#OPTIONAL
IgnoreNicks="mmbot spammer2"
###################################################################
#Gitter section
#Best to make a dedicated gitter account for the bot.
###################################################################
[Gitter]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#Token to connect with Gitter API
#You can get your token by going to https://developer.gitter.im/docs/welcome and SIGN IN
#REQUIRED
Token="Yourtokenhere"
#Nicks you want to ignore. Messages of those users will not be bridged.
#OPTIONAL
IgnoreNicks="spammer1 spammer2"
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#OPTIONAL (default {BRIDGE}-{NICK})
RemoteNickFormat="[{BRIDGE}] <{NICK}>
###################################################################
#slack section
###################################################################
[slack]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#### Settings for webhook matterbridge.
#### These settings will not be used when useAPI is enabled
#Url is your incoming webhook url as specified in slack
#See account settings - integrations - incoming webhooks on slack
#REQUIRED (unless useAPI=true)
URL="https://hooks.slack.com/services/yourhook"
#Address to listen on for outgoing webhook requests from slack
#See account settings - integrations - outgoing webhooks on slack
#This setting will not be used when useAPI is eanbled
#webhooks
#REQUIRED (unless useAPI=true)
BindAddress="0.0.0.0:9999"
#Icon that will be showed in slack
#OPTIONAL
IconURL="http://youricon.png"
#### Settings for using slack API
#OPTIONAL
useAPI=false
#Token to connect with the Slack API
#REQUIRED (when useAPI=true)
Token="yourslacktoken"
#### Shared settings for webhooks and API
#Whether to prefix messages from other bridges to mattermost with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the
#slack server. If you set PrefixMessagesWithNick to true, each message
#from bridge to Slack will by default be prefixed by "bridge-" + nick. You can,
#however, modify how the messages appear, by setting (and modifying) RemoteNickFormat
#OPTIONAL (default false)
PrefixMessagesWithNick=false
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#OPTIONAL (default {BRIDGE}-{NICK})
RemoteNickFormat="[{BRIDGE}] <{NICK}>
#how to format the list of IRC nicks when displayed in slack
#Possible options are "table" and "plain"
#OPTIONAL (default plain)
NickFormatter=plain
#How many nicks to list per row for formatters that support this.
#OPTIONAL (default 4)
NicksPerRow=4
#Nicks you want to ignore. Messages from those users will not be bridged.
#OPTIONAL
IgnoreNicks="mmbot spammer2"
@ -130,14 +256,24 @@ IgnoreNicks="mmbot spammer2"
#The name is just an identifier for you.
#REQUIRED (at least 1 channel)
[Channel "channel1"]
#Choose the IRC channel to send mattermost messages to.
#Choose the IRC channel to send messages to.
IRC="#off-topic"
#Choose the mattermost channel to send IRC messages to.
#Choose the mattermost channel to messages to.
mattermost="off-topic"
#Choose the xmpp channel to send messages to.
xmpp="off-topic"
#Choose the Gitter channel to send messages to.
#Gitter channels are named "user/repo"
gitter="42wim/matterbridge"
#Choose the slack channel to send messages to.
slack="general"
[Channel "testchannel"]
IRC="#testing"
mattermost="testing"
xmpp="testing"
gitter="user/repo"
slack="testing"
###################################################################
#general
@ -146,3 +282,6 @@ mattermost="testing"
#request your API key on https://github.com/giphy/GiphyAPI. This is a public beta key.
#OPTIONAL
GiphyApiKey="dc6zaTOxFJmzC"
#Enabling plus means you'll use the API version instead of the webhooks one
Plus=false

View File

@ -4,10 +4,11 @@ import (
"flag"
"fmt"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
)
var version = "0.5.0-beta2"
var version = "0.6.1"
func init() {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
@ -17,7 +18,7 @@ func main() {
flagConfig := flag.String("conf", "matterbridge.conf", "config file")
flagDebug := flag.Bool("debug", false, "enable debug")
flagVersion := flag.Bool("version", false, "show version")
flagPlus := flag.Bool("plus", false, "running using API instead of webhooks")
flagPlus := flag.Bool("plus", false, "running using API instead of webhooks (deprecated, set Plus flag in [general] config)")
flag.Parse()
if *flagVersion {
fmt.Println("version:", version)
@ -29,10 +30,12 @@ func main() {
log.SetLevel(log.DebugLevel)
}
fmt.Println("running version", version)
cfg := config.NewConfig(*flagConfig)
if *flagPlus {
bridge.NewBridge("matterbot", bridge.NewConfig(*flagConfig), "")
} else {
bridge.NewBridge("matterbot", bridge.NewConfig(*flagConfig), "legacy")
cfg.General.Plus = true
}
err := bridge.NewBridge(cfg)
if err != nil {
log.Debugf("starting bridge failed %#v", err)
}
select {}
}

View File

@ -2,6 +2,7 @@ package matterclient
import (
"crypto/tls"
"encoding/json"
"errors"
"net/http"
"net/http/cookiejar"
@ -27,7 +28,7 @@ type Credentials struct {
}
type Message struct {
Raw *model.Message
Raw *model.WebSocketEvent
Post *model.Post
Team string
Channel string
@ -49,14 +50,16 @@ type MMClient struct {
Team *Team
OtherTeams []*Team
Client *model.Client
WsClient *websocket.Conn
WsQuit bool
WsAway bool
WsConnected bool
User *model.User
Users map[string]*model.User
MessageChan chan *Message
log *log.Entry
WsClient *websocket.Conn
WsQuit bool
WsAway bool
WsConnected bool
WsSequence int64
WsPingChan chan *model.WebSocketResponse
}
func New(login, pass, team, server string) *MMClient {
@ -151,7 +154,7 @@ func (m *MMClient) Login() error {
m.Client.SetTeamId(m.Team.Id)
// setup websocket connection
wsurl := wsScheme + m.Credentials.Server + "/api/v3/users/websocket"
wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX + "/users/websocket"
header := http.Header{}
header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
@ -169,6 +172,8 @@ func (m *MMClient) Login() error {
}
b.Reset()
m.WsSequence = 1
m.WsPingChan = make(chan *model.WebSocketResponse)
// only start to parse WS messages when login is completely done
m.WsConnected = true
@ -180,7 +185,6 @@ func (m *MMClient) Logout() error {
m.WsQuit = true
m.WsClient.Close()
m.WsClient.UnderlyingConn().Close()
m.WsClient = nil
_, err := m.Client.Logout()
if err != nil {
return err
@ -190,42 +194,46 @@ func (m *MMClient) Logout() error {
func (m *MMClient) WsReceiver() {
for {
var rmsg model.Message
var rawMsg json.RawMessage
var err error
if m.WsQuit {
m.log.Debug("exiting WsReceiver")
return
}
if err := m.WsClient.ReadJSON(&rmsg); err != nil {
if !m.WsConnected {
time.Sleep(time.Millisecond * 100)
continue
}
if _, rawMsg, err = m.WsClient.ReadMessage(); err != nil {
m.log.Error("error:", err)
// reconnect
m.Login()
}
// we're not fully logged in yet.
if !m.WsConnected {
var event model.WebSocketEvent
if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() {
m.log.Debugf("WsReceiver: %#v", event)
msg := &Message{Raw: &event, Team: m.Credentials.Team}
m.parseMessage(msg)
m.MessageChan <- msg
continue
}
if rmsg.Action == "ping" {
m.handleWsPing()
var response model.WebSocketResponse
if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
m.log.Debugf("WsReceiver: %#v", response)
m.parseResponse(response)
continue
}
msg := &Message{Raw: &rmsg, Team: m.Credentials.Team}
m.parseMessage(msg)
m.MessageChan <- msg
}
}
func (m *MMClient) handleWsPing() {
m.log.Debug("Ws PING")
if !m.WsQuit && !m.WsAway {
m.log.Debug("Ws PONG")
m.WsClient.WriteMessage(websocket.PongMessage, []byte{})
}
}
func (m *MMClient) parseMessage(rmsg *Message) {
switch rmsg.Raw.Action {
case model.ACTION_POSTED:
switch rmsg.Raw.Event {
case model.WEBSOCKET_EVENT_POSTED:
m.parseActionPost(rmsg)
/*
case model.ACTION_USER_REMOVED:
@ -236,8 +244,17 @@ func (m *MMClient) parseMessage(rmsg *Message) {
}
}
func (m *MMClient) parseResponse(rmsg model.WebSocketResponse) {
if rmsg.Data != nil {
// ping reply
if rmsg.Data["text"].(string) == "pong" {
m.WsPingChan <- &rmsg
}
}
}
func (m *MMClient) parseActionPost(rmsg *Message) {
data := model.PostFromJson(strings.NewReader(rmsg.Raw.Props["post"]))
data := model.PostFromJson(strings.NewReader(rmsg.Raw.Data["post"].(string)))
// we don't have the user, refresh the userlist
if m.GetUser(data.UserId) == nil {
m.UpdateUsers()
@ -246,7 +263,7 @@ func (m *MMClient) parseActionPost(rmsg *Message) {
rmsg.Channel = m.GetChannelName(data.ChannelId)
rmsg.Team = m.GetTeamName(rmsg.Raw.TeamId)
// direct message
if data.Type == "D" {
if rmsg.Raw.Data["channel_type"] == "D" {
rmsg.Channel = m.GetUser(data.UserId).Username
}
rmsg.Text = data.Message
@ -255,7 +272,10 @@ func (m *MMClient) parseActionPost(rmsg *Message) {
}
func (m *MMClient) UpdateUsers() error {
mmusers, _ := m.Client.GetProfilesForDirectMessageList(m.Team.Id)
mmusers, err := m.Client.GetProfilesForDirectMessageList(m.Team.Id)
if err != nil {
return errors.New(err.DetailedError)
}
m.Lock()
m.Users = mmusers.Data.(map[string]*model.User)
m.Unlock()
@ -263,8 +283,14 @@ func (m *MMClient) UpdateUsers() error {
}
func (m *MMClient) UpdateChannels() error {
mmchannels, _ := m.Client.GetChannels("")
mmchannels2, _ := m.Client.GetMoreChannels("")
mmchannels, err := m.Client.GetChannels("")
if err != nil {
return errors.New(err.DetailedError)
}
mmchannels2, err := m.Client.GetMoreChannels("")
if err != nil {
return errors.New(err.DetailedError)
}
m.Lock()
m.Team.Channels = mmchannels.Data.(*model.ChannelList)
m.Team.MoreChannels = mmchannels2.Data.(*model.ChannelList)
@ -396,7 +422,7 @@ func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
func (m *MMClient) UpdateLastViewed(channelId string) {
m.log.Debugf("posting lastview %#v", channelId)
_, err := m.Client.UpdateLastViewedAt(channelId)
_, err := m.Client.UpdateLastViewedAt(channelId, true)
if err != nil {
m.log.Error(err)
}
@ -498,6 +524,7 @@ func (m *MMClient) GetTeamFromChannel(channelId string) string {
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.Channels.Channels...)
channels = append(channels, t.MoreChannels.Channels...)
for _, c := range channels {
if c.Id == channelId {
return t.Id
@ -534,6 +561,42 @@ func (m *MMClient) GetUser(userId string) *model.User {
return m.Users[userId]
}
func (m *MMClient) GetStatus(userId string) string {
res, err := m.Client.GetStatuses()
if err != nil {
return ""
}
status := res.Data.(map[string]string)
if status[userId] == model.STATUS_AWAY {
return "away"
}
if status[userId] == model.STATUS_ONLINE {
return "online"
}
return "offline"
}
func (m *MMClient) StatusLoop() {
for {
if m.WsQuit {
return
}
if m.WsConnected {
m.log.Debug("WS PING")
m.sendWSRequest("ping", nil)
select {
case <-m.WsPingChan:
m.log.Debug("WS PONG received")
case <-time.After(time.Second * 5):
m.Logout()
m.WsQuit = false
m.Login()
}
}
time.Sleep(time.Second * 60)
}
}
// initialize user and teams
func (m *MMClient) initUser() error {
m.Lock()
@ -568,3 +631,14 @@ func (m *MMClient) initUser() error {
}
return nil
}
func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) error {
req := &model.WebSocketRequest{}
req.Seq = m.WsSequence
req.Action = action
req.Data = data
m.WsSequence++
m.log.Debugf("sendWsRequest %#v", req)
m.WsClient.WriteJSON(req)
return nil
}

View File

@ -27,6 +27,8 @@ type OMessage struct {
// IMessage for mattermost outgoing webhook. (received from mattermost)
type IMessage struct {
BotID string `schema:"bot_id"`
BotName string `schema:"bot_name"`
Token string `schema:"token"`
TeamID string `schema:"team_id"`
TeamDomain string `schema:"team_domain"`
@ -36,6 +38,8 @@ type IMessage struct {
UserID string `schema:"user_id"`
UserName string `schema:"user_name"`
PostId string `schema:"post_id"`
RawText string `schema:"raw_text"`
ServiceId string `schema:"service_id"`
Text string `schema:"text"`
TriggerWord string `schema:"trigger_word"`
}

View File

@ -0,0 +1,20 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import "github.com/mattermost/platform/model"
type AccountMigrationInterface interface {
MigrateToLdap(fromAuthService string, forignUserFieldNameToMatch string) *model.AppError
}
var theAccountMigrationInterface AccountMigrationInterface
func RegisterAccountMigrationInterface(newInterface AccountMigrationInterface) {
theAccountMigrationInterface = newInterface
}
func GetAccountMigrationInterface() AccountMigrationInterface {
return theAccountMigrationInterface
}

View File

@ -0,0 +1,32 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import (
"github.com/mattermost/platform/model"
)
type ClusterInterface interface {
StartInterNodeCommunication()
StopInterNodeCommunication()
GetClusterInfos() []*model.ClusterInfo
RemoveAllSessionsForUserId(userId string)
InvalidateCacheForUser(userId string)
InvalidateCacheForChannel(channelId string)
Publish(event *model.WebSocketEvent)
UpdateStatus(status *model.Status)
GetLogs() ([]string, *model.AppError)
GetClusterId() string
ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError
}
var theClusterInterface ClusterInterface
func RegisterClusterInterface(newInterface ClusterInterface) {
theClusterInterface = newInterface
}
func GetClusterInterface() ClusterInterface {
return theClusterInterface
}

View File

@ -15,6 +15,9 @@ type LdapInterface interface {
ValidateFilter(filter string) *model.AppError
Syncronize() *model.AppError
StartLdapSyncJob()
SyncNow()
RunTest() *model.AppError
GetAllLdapUsers() ([]*model.User, *model.AppError)
}
var theLdapInterface LdapInterface

View File

@ -15,10 +15,12 @@ const (
)
type AccessData struct {
AuthCode string `json:"auth_code"`
ClientId string `json:"client_id"`
UserId string `json:"user_id"`
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
RedirectUri string `json:"redirect_uri"`
ExpiresAt int64 `json:"expires_at"`
}
type AccessResponse struct {
@ -33,8 +35,12 @@ type AccessResponse struct {
// correctly.
func (ad *AccessData) IsValid() *AppError {
if len(ad.AuthCode) == 0 || len(ad.AuthCode) > 128 {
return NewLocAppError("AccessData.IsValid", "model.access.is_valid.auth_code.app_error", nil, "")
if len(ad.ClientId) == 0 || len(ad.ClientId) > 26 {
return NewLocAppError("AccessData.IsValid", "model.access.is_valid.client_id.app_error", nil, "")
}
if len(ad.UserId) == 0 || len(ad.UserId) > 26 {
return NewLocAppError("AccessData.IsValid", "model.access.is_valid.user_id.app_error", nil, "")
}
if len(ad.Token) != 26 {
@ -52,6 +58,19 @@ func (ad *AccessData) IsValid() *AppError {
return nil
}
func (me *AccessData) IsExpired() bool {
if me.ExpiresAt <= 0 {
return false
}
if GetMillis() > me.ExpiresAt {
return true
}
return false
}
func (ad *AccessData) ToJson() string {
b, err := json.Marshal(ad)
if err != nil {

View File

@ -11,6 +11,7 @@ import (
const (
AUTHCODE_EXPIRE_TIME = 60 * 10 // 10 minutes
AUTHCODE_RESPONSE_TYPE = "code"
DEFAULT_SCOPE = "user"
)
type AuthData struct {
@ -71,6 +72,10 @@ func (ad *AuthData) PreSave() {
if ad.CreateAt == 0 {
ad.CreateAt = GetMillis()
}
if len(ad.Scope) == 0 {
ad.Scope = DEFAULT_SCOPE
}
}
func (ad *AuthData) ToJson() string {

View File

@ -124,9 +124,6 @@ func (o *Channel) ExtraUpdated() {
o.ExtraUpdateAt = GetMillis()
}
func (o *Channel) PreExport() {
}
func GetDMNameFromIds(userId1, userId2 string) string {
if userId1 > userId2 {
return userId2 + "__" + userId1

View File

@ -20,6 +20,7 @@ import (
const (
HEADER_REQUEST_ID = "X-Request-ID"
HEADER_VERSION_ID = "X-Version-ID"
HEADER_CLUSTER_ID = "X-Cluster-ID"
HEADER_ETAG_SERVER = "ETag"
HEADER_ETAG_CLIENT = "If-None-Match"
HEADER_FORWARDED = "X-Forwarded-For"
@ -32,6 +33,9 @@ const (
HEADER_REQUESTED_WITH_XML = "XMLHttpRequest"
STATUS = "status"
STATUS_OK = "OK"
STATUS_FAIL = "FAIL"
CLIENT_DIR = "webapp/dist"
API_URL_SUFFIX_V1 = "/api/v1"
API_URL_SUFFIX_V3 = "/api/v3"
@ -276,6 +280,9 @@ func (c *Client) GetPing() (map[string]string, *AppError) {
// Team Routes Section
// SignupTeam sends an email with a team sign-up link to the provided address if email
// verification is enabled, otherwise it returns a map with a "follow_link" entry
// containing the team sign-up link.
func (c *Client) SignupTeam(email string, displayName string) (*Result, *AppError) {
m := make(map[string]string)
m["email"] = email
@ -289,6 +296,8 @@ func (c *Client) SignupTeam(email string, displayName string) (*Result, *AppErro
}
}
// CreateTeamFromSignup creates a team based on the provided TeamSignup struct. On success
// it returns the TeamSignup struct.
func (c *Client) CreateTeamFromSignup(teamSignup *TeamSignup) (*Result, *AppError) {
if r, err := c.DoApiPost("/teams/create_from_signup", teamSignup.ToJson()); err != nil {
return nil, err
@ -299,6 +308,8 @@ func (c *Client) CreateTeamFromSignup(teamSignup *TeamSignup) (*Result, *AppErro
}
}
// CreateTeam creates a team based on the provided Team struct. On success it returns
// the Team struct with the Id, CreateAt and other server-decided fields populated.
func (c *Client) CreateTeam(team *Team) (*Result, *AppError) {
if r, err := c.DoApiPost("/teams/create", team.ToJson()); err != nil {
return nil, err
@ -309,6 +320,7 @@ func (c *Client) CreateTeam(team *Team) (*Result, *AppError) {
}
}
// GetAllTeams returns a map of all teams using team ids as the key.
func (c *Client) GetAllTeams() (*Result, *AppError) {
if r, err := c.DoApiGet("/teams/all", "", ""); err != nil {
return nil, err
@ -319,6 +331,8 @@ func (c *Client) GetAllTeams() (*Result, *AppError) {
}
}
// GetAllTeamListings returns a map of all teams that are available to join
// using team ids as the key. Must be authenticated.
func (c *Client) GetAllTeamListings() (*Result, *AppError) {
if r, err := c.DoApiGet("/teams/all_team_listings", "", ""); err != nil {
return nil, err
@ -329,6 +343,8 @@ func (c *Client) GetAllTeamListings() (*Result, *AppError) {
}
}
// FindTeamByName returns the strings "true" or "false" depending on if a team
// with the provided name was found.
func (c *Client) FindTeamByName(name string) (*Result, *AppError) {
m := make(map[string]string)
m["name"] = name
@ -365,6 +381,8 @@ func (c *Client) AddUserToTeam(teamId string, userId string) (*Result, *AppError
}
}
// AddUserToTeamFromInvite adds a user to a team based off data provided in an invite link.
// Either hash and dataToHash are required or inviteId is required.
func (c *Client) AddUserToTeamFromInvite(hash, dataToHash, inviteId string) (*Result, *AppError) {
data := make(map[string]string)
data["hash"] = hash
@ -409,6 +427,9 @@ func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) {
}
}
// UpdateTeam updates a team based on the changes in the provided team struct. On success
// it returns a sanitized version of the updated team. Must be authenticated as a team admin
// for that team or a system admin.
func (c *Client) UpdateTeam(team *Team) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetTeamRoute()+"/update", team.ToJson()); err != nil {
return nil, err
@ -419,6 +440,9 @@ func (c *Client) UpdateTeam(team *Team) (*Result, *AppError) {
}
}
// User Routes Section
// CreateUser creates a user in the system based on the provided user struct.
func (c *Client) CreateUser(user *User, hash string) (*Result, *AppError) {
if r, err := c.DoApiPost("/users/create", user.ToJson()); err != nil {
return nil, err
@ -429,6 +453,8 @@ func (c *Client) CreateUser(user *User, hash string) (*Result, *AppError) {
}
}
// CreateUserWithInvite creates a user based on the provided user struct. Either the hash and
// data strings or the inviteId is required from the invite.
func (c *Client) CreateUserWithInvite(user *User, hash string, data string, inviteId string) (*Result, *AppError) {
url := "/users/create?d=" + url.QueryEscape(data) + "&h=" + url.QueryEscape(hash) + "&iid=" + url.QueryEscape(inviteId)
@ -452,6 +478,7 @@ func (c *Client) CreateUserFromSignup(user *User, data string, hash string) (*Re
}
}
// GetUser returns a user based on a provided user id string. Must be authenticated.
func (c *Client) GetUser(id string, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet("/users/"+id+"/get", "", etag); err != nil {
return nil, err
@ -462,6 +489,7 @@ func (c *Client) GetUser(id string, etag string) (*Result, *AppError) {
}
}
// GetMe returns the current user.
func (c *Client) GetMe(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet("/users/me", "", etag); err != nil {
return nil, err
@ -472,6 +500,8 @@ func (c *Client) GetMe(etag string) (*Result, *AppError) {
}
}
// GetProfilesForDirectMessageList returns a map of users for a team that can be direct
// messaged, using user id as the key. Must be authenticated.
func (c *Client) GetProfilesForDirectMessageList(teamId string) (*Result, *AppError) {
if r, err := c.DoApiGet("/users/profiles_for_dm_list/"+teamId, "", ""); err != nil {
return nil, err
@ -482,6 +512,8 @@ func (c *Client) GetProfilesForDirectMessageList(teamId string) (*Result, *AppEr
}
}
// GetProfiles returns a map of users for a team using user id as the key. Must
// be authenticated.
func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet("/users/profiles/"+teamId, "", etag); err != nil {
return nil, err
@ -492,6 +524,8 @@ func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
}
}
// GetDirectProfiles gets a map of users that are currently shown in the sidebar,
// using user id as the key. Must be authenticated.
func (c *Client) GetDirectProfiles(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet("/users/direct_profiles", "", etag); err != nil {
return nil, err
@ -502,6 +536,7 @@ func (c *Client) GetDirectProfiles(etag string) (*Result, *AppError) {
}
}
// LoginById authenticates a user by user id and password.
func (c *Client) LoginById(id string, password string) (*Result, *AppError) {
m := make(map[string]string)
m["id"] = id
@ -509,6 +544,8 @@ func (c *Client) LoginById(id string, password string) (*Result, *AppError) {
return c.login(m)
}
// Login authenticates a user by login id, which can be username, email or some sort
// of SSO identifier based on configuration, and a password.
func (c *Client) Login(loginId string, password string) (*Result, *AppError) {
m := make(map[string]string)
m["login_id"] = loginId
@ -516,6 +553,7 @@ func (c *Client) Login(loginId string, password string) (*Result, *AppError) {
return c.login(m)
}
// LoginByLdap authenticates a user by LDAP id and password.
func (c *Client) LoginByLdap(loginId string, password string) (*Result, *AppError) {
m := make(map[string]string)
m["login_id"] = loginId
@ -524,6 +562,9 @@ func (c *Client) LoginByLdap(loginId string, password string) (*Result, *AppErro
return c.login(m)
}
// LoginWithDevice authenticates a user by login id (username, email or some sort
// of SSO identifier based on configuration), password and attaches a device id to
// the session.
func (c *Client) LoginWithDevice(loginId string, password string, deviceId string) (*Result, *AppError) {
m := make(map[string]string)
m["login_id"] = loginId
@ -550,6 +591,7 @@ func (c *Client) login(m map[string]string) (*Result, *AppError) {
}
}
// Logout terminates the current user's session.
func (c *Client) Logout() (*Result, *AppError) {
if r, err := c.DoApiPost("/users/logout", ""); err != nil {
return nil, err
@ -564,6 +606,9 @@ func (c *Client) Logout() (*Result, *AppError) {
}
}
// CheckMfa returns a map with key "mfa_required" with the string value "true" or "false",
// indicating whether MFA is required to log the user in, based on a provided login id
// (username, email or some sort of SSO identifier based on configuration).
func (c *Client) CheckMfa(loginId string) (*Result, *AppError) {
m := make(map[string]string)
m["login_id"] = loginId
@ -577,6 +622,8 @@ func (c *Client) CheckMfa(loginId string) (*Result, *AppError) {
}
}
// GenerateMfaQrCode returns a QR code imagem containing the secret, to be scanned
// by a multi-factor authentication mobile application. Must be authenticated.
func (c *Client) GenerateMfaQrCode() (*Result, *AppError) {
if r, err := c.DoApiGet("/users/generate_mfa_qr", "", ""); err != nil {
return nil, err
@ -587,6 +634,9 @@ func (c *Client) GenerateMfaQrCode() (*Result, *AppError) {
}
}
// UpdateMfa activates multi-factor authenticates for the current user if activate
// is true and a valid token is provided. If activate is false, then token is not
// required and multi-factor authentication is disabled for the current user.
func (c *Client) UpdateMfa(activate bool, token string) (*Result, *AppError) {
m := make(map[string]interface{})
m["activate"] = activate
@ -761,6 +811,26 @@ func (c *Client) GetLogs() (*Result, *AppError) {
}
}
func (c *Client) GetClusterStatus() ([]*ClusterInfo, *AppError) {
if r, err := c.DoApiGet("/admin/cluster_status", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return ClusterInfosFromJson(r.Body), nil
}
}
// GetRecentlyActiveUsers returns a map of users including lastActivityAt using user id as the key
func (c *Client) GetRecentlyActiveUsers(teamId string) (*Result, *AppError) {
if r, err := c.DoApiGet("/admin/recently_active_users/"+teamId, "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil
}
}
func (c *Client) GetAllAudits() (*Result, *AppError) {
if r, err := c.DoApiGet("/admin/audits", "", ""); err != nil {
return nil, err
@ -828,6 +898,19 @@ func (c *Client) TestEmail(config *Config) (*Result, *AppError) {
}
}
// TestLdap will run a connection test on the current LDAP settings.
// It will return the standard OK response if settings work. Otherwise
// it will return an appropriate error.
func (c *Client) TestLdap(config *Config) (*Result, *AppError) {
if r, err := c.DoApiPost("/admin/ldap_test", config.ToJson()); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
func (c *Client) GetComplianceReports() (*Result, *AppError) {
if r, err := c.DoApiGet("/admin/compliance_reports", "", ""); err != nil {
return nil, err
@ -1068,8 +1151,13 @@ func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) {
}
}
func (c *Client) UpdateLastViewedAt(channelId string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/update_last_viewed_at", ""); err != nil {
// UpdateLastViewedAt will mark a channel as read.
// The channelId indicates the channel to mark as read. If active is true, push notifications
// will be cleared if there are unread messages. The default for active is true.
func (c *Client) UpdateLastViewedAt(channelId string, active bool) (*Result, *AppError) {
data := make(map[string]interface{})
data["active"] = active
if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/update_last_viewed_at", StringInterfaceToJson(data)); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -1181,6 +1269,18 @@ func (c *Client) SearchPosts(terms string, isOrSearch bool) (*Result, *AppError)
}
}
// GetFlaggedPosts will return a post list of posts that have been flagged by the user.
// The page is set by the integer parameters offset and limit.
func (c *Client) GetFlaggedPosts(offset int, limit int) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+fmt.Sprintf("/posts/flagged/%v/%v", offset, limit), "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil
}
}
func (c *Client) UploadProfileFile(data []byte, contentType string) (*Result, *AppError) {
return c.uploadFile(c.ApiUrl+"/users/newimage", data, contentType)
}
@ -1368,8 +1468,24 @@ func (c *Client) AdminResetPassword(userId, newPassword string) (*Result, *AppEr
}
}
func (c *Client) GetStatuses(data []string) (*Result, *AppError) {
if r, err := c.DoApiPost("/users/status", ArrayToJson(data)); err != nil {
// GetStatuses returns a map of string statuses using user id as the key
func (c *Client) GetStatuses() (*Result, *AppError) {
if r, err := c.DoApiGet("/users/status", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
// SetActiveChannel sets the the channel id the user is currently viewing.
// The channelId key is required but the value can be blank. Returns standard
// response.
func (c *Client) SetActiveChannel(channelId string) (*Result, *AppError) {
data := map[string]string{}
data["channel_id"] = channelId
if r, err := c.DoApiPost("/users/status/set_active_channel", MapToJson(data)); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -1398,6 +1514,8 @@ func (c *Client) GetTeamMembers(teamId string) (*Result, *AppError) {
}
}
// RegisterApp creates a new OAuth2 app to be used with the OAuth2 Provider. On success
// it returns the created app. Must be authenticated as a user.
func (c *Client) RegisterApp(app *OAuthApp) (*Result, *AppError) {
if r, err := c.DoApiPost("/oauth/register", app.ToJson()); err != nil {
return nil, err
@ -1408,6 +1526,9 @@ func (c *Client) RegisterApp(app *OAuthApp) (*Result, *AppError) {
}
}
// AllowOAuth allows a new session by an OAuth2 App. On success
// it returns the url to be redirected back to the app which initiated the oauth2 flow.
// Must be authenticated as a user.
func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (*Result, *AppError) {
if r, err := c.DoApiGet("/oauth/allow?response_type="+rspType+"&client_id="+clientId+"&redirect_uri="+url.QueryEscape(redirect)+"&scope="+scope+"&state="+url.QueryEscape(state), "", ""); err != nil {
return nil, err
@ -1418,8 +1539,83 @@ func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (*
}
}
// GetOAuthAppsByUser returns the OAuth2 Apps registered by the user. On success
// it returns a list of OAuth2 Apps from the same user or all the registered apps if the user
// is a System Administrator. Must be authenticated as a user.
func (c *Client) GetOAuthAppsByUser() (*Result, *AppError) {
if r, err := c.DoApiGet("/oauth/list", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), OAuthAppListFromJson(r.Body)}, nil
}
}
// GetOAuthAppInfo lookup an OAuth2 App using the client_id. On success
// it returns a Sanitized OAuth2 App. Must be authenticated as a user.
func (c *Client) GetOAuthAppInfo(clientId string) (*Result, *AppError) {
if r, err := c.DoApiGet("/oauth/app/"+clientId, "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), OAuthAppFromJson(r.Body)}, nil
}
}
// DeleteOAuthApp deletes an OAuth2 app, the app must be deleted by the same user who created it or
// a System Administrator. On success returs Status OK. Must be authenticated as a user.
func (c *Client) DeleteOAuthApp(id string) (*Result, *AppError) {
data := make(map[string]string)
data["id"] = id
if r, err := c.DoApiPost("/oauth/delete", MapToJson(data)); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
// GetOAuthAuthorizedApps returns the OAuth2 Apps authorized by the user. On success
// it returns a list of sanitized OAuth2 Authorized Apps by the user.
func (c *Client) GetOAuthAuthorizedApps() (*Result, *AppError) {
if r, err := c.DoApiGet("/oauth/authorized", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), OAuthAppListFromJson(r.Body)}, nil
}
}
// OAuthDeauthorizeApp deauthorize a user an OAuth 2.0 app. On success
// it returns status OK or an AppError on fail.
func (c *Client) OAuthDeauthorizeApp(clientId string) *AppError {
if r, err := c.DoApiPost("/oauth/"+clientId+"/deauthorize", ""); err != nil {
return err
} else {
defer closeBody(r)
return nil
}
}
// RegenerateOAuthAppSecret generates a new OAuth App Client Secret. On success
// it returns an OAuth2 App. Must be authenticated as a user and the same user who
// registered the app or a System Admin.
func (c *Client) RegenerateOAuthAppSecret(clientId string) (*Result, *AppError) {
if r, err := c.DoApiPost("/oauth/"+clientId+"/regen_secret", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), OAuthAppFromJson(r.Body)}, nil
}
}
func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) {
if r, err := c.DoApiPost("/oauth/access_token", data.Encode()); err != nil {
if r, err := c.DoPost("/oauth/access_token", data.Encode(), "application/x-www-form-urlencoded"); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -1509,6 +1705,16 @@ func (c *Client) GetPreferenceCategory(category string) (*Result, *AppError) {
}
}
// DeletePreferences deletes a list of preferences owned by the current user. If successful,
// it will return status=ok. Otherwise, an error will be returned.
func (c *Client) DeletePreferences(preferences *Preferences) (bool, *AppError) {
if r, err := c.DoApiPost("/preferences/delete", preferences.ToJson()); err != nil {
return false, err
} else {
return c.CheckStatusOK(r), nil
}
}
func (c *Client) CreateOutgoingWebhook(hook *OutgoingWebhook) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/outgoing/create", hook.ToJson()); err != nil {
return nil, err
@ -1648,3 +1854,47 @@ func (c *Client) DeleteEmoji(id string) (bool, *AppError) {
func (c *Client) GetCustomEmojiImageUrl(id string) string {
return c.GetEmojiRoute() + "/" + id
}
// Uploads a x509 base64 Certificate or Private Key file to be used with SAML.
// data byte array is required and needs to be a Multi-Part with 'certificate' as the field name
// contentType is also required. Returns nil if succesful, otherwise returns an AppError
func (c *Client) UploadCertificateFile(data []byte, contentType string) *AppError {
url := c.ApiUrl + "/admin/add_certificate"
rq, _ := http.NewRequest("POST", url, bytes.NewReader(data))
rq.Header.Set("Content-Type", contentType)
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
}
if rp, err := c.HttpClient.Do(rq); err != nil {
return NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error())
} else if rp.StatusCode >= 300 {
return AppErrorFromJson(rp.Body)
} else {
defer closeBody(rp)
return nil
}
}
// Removes a x509 base64 Certificate or Private Key file used with SAML.
// filename is required. Returns nil if successful, otherwise returns an AppError
func (c *Client) RemoveCertificateFile(filename string) *AppError {
if r, err := c.DoApiPost("/admin/remove_certificate", MapToJson(map[string]string{"filename": filename})); err != nil {
return err
} else {
defer closeBody(r)
return nil
}
}
// Checks if the x509 base64 Certificates and Private Key files used with SAML exists on the file system.
// Returns a map[string]interface{} if successful, otherwise returns an AppError. Must be System Admin authenticated.
func (c *Client) SamlCertificateStatus(filename string) (map[string]interface{}, *AppError) {
if r, err := c.DoApiGet("/admin/remove_certificate", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return StringInterfaceFromJson(r.Body), nil
}
}

View File

@ -0,0 +1,66 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type ClusterInfo struct {
Id string `json:"id"`
Version string `json:"version"`
ConfigHash string `json:"config_hash"`
InterNodeUrl string `json:"internode_url"`
Hostname string `json:"hostname"`
LastSuccessfulPing int64 `json:"last_ping"`
IsAlive bool `json:"is_alive"`
}
func (me *ClusterInfo) ToJson() string {
b, err := json.Marshal(me)
if err != nil {
return ""
} else {
return string(b)
}
}
func ClusterInfoFromJson(data io.Reader) *ClusterInfo {
decoder := json.NewDecoder(data)
var me ClusterInfo
err := decoder.Decode(&me)
if err == nil {
return &me
} else {
return nil
}
}
func (me *ClusterInfo) HaveEstablishedInitialContact() bool {
if me.Id != "" {
return true
}
return false
}
func ClusterInfosToJson(objmap []*ClusterInfo) string {
if b, err := json.Marshal(objmap); err != nil {
return ""
} else {
return string(b)
}
}
func ClusterInfosFromJson(data io.Reader) []*ClusterInfo {
decoder := json.NewDecoder(data)
var objmap []*ClusterInfo
if err := decoder.Decode(&objmap); err != nil {
return make([]*ClusterInfo, 0)
} else {
return objmap
}
}

View File

@ -6,10 +6,12 @@ package model
import (
"encoding/json"
"io"
"net/url"
)
const (
CONN_SECURITY_NONE = ""
CONN_SECURITY_PLAIN = "PLAIN"
CONN_SECURITY_TLS = "TLS"
CONN_SECURITY_STARTTLS = "STARTTLS"
@ -22,8 +24,9 @@ const (
PASSWORD_MAXIMUM_LENGTH = 64
PASSWORD_MINIMUM_LENGTH = 5
SERVICE_GITLAB = "gitlab"
SERVICE_GOOGLE = "google"
SERVICE_GITLAB = "gitlab"
SERVICE_GOOGLE = "google"
SERVICE_OFFICE365 = "office365"
WEBSERVER_MODE_REGULAR = "regular"
WEBSERVER_MODE_GZIP = "gzip"
@ -44,9 +47,15 @@ const (
RESTRICT_EMOJI_CREATION_ALL = "all"
RESTRICT_EMOJI_CREATION_ADMIN = "admin"
RESTRICT_EMOJI_CREATION_SYSTEM_ADMIN = "system_admin"
EMAIL_BATCHING_BUFFER_SIZE = 256
EMAIL_BATCHING_INTERVAL = 30
SITENAME_MAX_LENGTH = 30
)
type ServiceSettings struct {
SiteURL *string
ListenAddress string
MaximumLoginAttempts int
SegmentDeveloperKey string
@ -75,6 +84,12 @@ type ServiceSettings struct {
RestrictCustomEmojiCreation *string
}
type ClusterSettings struct {
Enable *bool
InterNodeListenAddress *string
InterNodeUrls []string
}
type SSOSettings struct {
Enable bool
Secret string
@ -103,6 +118,7 @@ type LogSettings struct {
FileFormat string
FileLocation string
EnableWebhookDebugging bool
EnableDiagnostics *bool
}
type PasswordSettings struct {
@ -118,7 +134,7 @@ type FileSettings struct {
DriverName string
Directory string
EnablePublicLink bool
PublicLinkSalt string
PublicLinkSalt *string
ThumbnailWidth int
ThumbnailHeight int
PreviewWidth int
@ -155,6 +171,9 @@ type EmailSettings struct {
SendPushNotifications *bool
PushNotificationServer *string
PushNotificationContents *string
EnableEmailBatching *bool
EmailBatchingBufferSize *int
EmailBatchingInterval *int
}
type RateLimitSettings struct {
@ -189,10 +208,12 @@ type TeamSettings struct {
RestrictTeamNames *bool
EnableCustomBrand *bool
CustomBrandText *string
CustomDescriptionText *string
RestrictDirectMessage *string
RestrictTeamInvite *string
RestrictPublicChannelManagement *string
RestrictPrivateChannelManagement *string
UserStatusAwayTimeout *int64
}
type LdapSettings struct {
@ -265,6 +286,12 @@ type SamlSettings struct {
LoginButtonText *string
}
type NativeAppSettings struct {
AppDownloadLink *string
AndroidAppDownloadLink *string
IosAppDownloadLink *string
}
type Config struct {
ServiceSettings ServiceSettings
TeamSettings TeamSettings
@ -278,10 +305,13 @@ type Config struct {
SupportSettings SupportSettings
GitLabSettings SSOSettings
GoogleSettings SSOSettings
Office365Settings SSOSettings
LdapSettings LdapSettings
ComplianceSettings ComplianceSettings
LocalizationSettings LocalizationSettings
SamlSettings SamlSettings
NativeAppSettings NativeAppSettings
ClusterSettings ClusterSettings
}
func (o *Config) ToJson() string {
@ -299,6 +329,8 @@ func (o *Config) GetSSOService(service string) *SSOSettings {
return &o.GitLabSettings
case SERVICE_GOOGLE:
return &o.GoogleSettings
case SERVICE_OFFICE365:
return &o.Office365Settings
}
return nil
@ -326,8 +358,9 @@ func (o *Config) SetDefaults() {
*o.FileSettings.MaxFileSize = 52428800 // 50 MB
}
if len(o.FileSettings.PublicLinkSalt) == 0 {
o.FileSettings.PublicLinkSalt = NewRandomString(32)
if len(*o.FileSettings.PublicLinkSalt) == 0 {
o.FileSettings.PublicLinkSalt = new(string)
*o.FileSettings.PublicLinkSalt = NewRandomString(32)
}
if o.FileSettings.AmazonS3LocationConstraint == nil {
@ -348,6 +381,11 @@ func (o *Config) SetDefaults() {
o.EmailSettings.PasswordResetSalt = NewRandomString(32)
}
if o.ServiceSettings.SiteURL == nil {
o.ServiceSettings.SiteURL = new(string)
*o.ServiceSettings.SiteURL = ""
}
if o.ServiceSettings.EnableDeveloper == nil {
o.ServiceSettings.EnableDeveloper = new(bool)
*o.ServiceSettings.EnableDeveloper = false
@ -408,6 +446,11 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.CustomBrandText = ""
}
if o.TeamSettings.CustomDescriptionText == nil {
o.TeamSettings.CustomDescriptionText = new(string)
*o.TeamSettings.CustomDescriptionText = ""
}
if o.TeamSettings.EnableOpenServer == nil {
o.TeamSettings.EnableOpenServer = new(bool)
*o.TeamSettings.EnableOpenServer = false
@ -433,6 +476,11 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.RestrictPrivateChannelManagement = PERMISSIONS_ALL
}
if o.TeamSettings.UserStatusAwayTimeout == nil {
o.TeamSettings.UserStatusAwayTimeout = new(int64)
*o.TeamSettings.UserStatusAwayTimeout = 300
}
if o.EmailSettings.EnableSignInWithEmail == nil {
o.EmailSettings.EnableSignInWithEmail = new(bool)
@ -468,13 +516,28 @@ func (o *Config) SetDefaults() {
*o.EmailSettings.FeedbackOrganization = ""
}
if o.EmailSettings.EnableEmailBatching == nil {
o.EmailSettings.EnableEmailBatching = new(bool)
*o.EmailSettings.EnableEmailBatching = false
}
if o.EmailSettings.EmailBatchingBufferSize == nil {
o.EmailSettings.EmailBatchingBufferSize = new(int)
*o.EmailSettings.EmailBatchingBufferSize = EMAIL_BATCHING_BUFFER_SIZE
}
if o.EmailSettings.EmailBatchingInterval == nil {
o.EmailSettings.EmailBatchingInterval = new(int)
*o.EmailSettings.EmailBatchingInterval = EMAIL_BATCHING_INTERVAL
}
if !IsSafeLink(o.SupportSettings.TermsOfServiceLink) {
o.SupportSettings.TermsOfServiceLink = nil
}
if o.SupportSettings.TermsOfServiceLink == nil {
o.SupportSettings.TermsOfServiceLink = new(string)
*o.SupportSettings.TermsOfServiceLink = "/static/help/terms.html"
*o.SupportSettings.TermsOfServiceLink = "https://about.mattermost.com/default-terms/"
}
if !IsSafeLink(o.SupportSettings.PrivacyPolicyLink) {
@ -483,7 +546,7 @@ func (o *Config) SetDefaults() {
if o.SupportSettings.PrivacyPolicyLink == nil {
o.SupportSettings.PrivacyPolicyLink = new(string)
*o.SupportSettings.PrivacyPolicyLink = "/static/help/privacy.html"
*o.SupportSettings.PrivacyPolicyLink = ""
}
if !IsSafeLink(o.SupportSettings.AboutLink) {
@ -492,7 +555,7 @@ func (o *Config) SetDefaults() {
if o.SupportSettings.AboutLink == nil {
o.SupportSettings.AboutLink = new(string)
*o.SupportSettings.AboutLink = "/static/help/about.html"
*o.SupportSettings.AboutLink = ""
}
if !IsSafeLink(o.SupportSettings.HelpLink) {
@ -501,7 +564,7 @@ func (o *Config) SetDefaults() {
if o.SupportSettings.HelpLink == nil {
o.SupportSettings.HelpLink = new(string)
*o.SupportSettings.HelpLink = "/static/help/help.html"
*o.SupportSettings.HelpLink = ""
}
if !IsSafeLink(o.SupportSettings.ReportAProblemLink) {
@ -510,7 +573,7 @@ func (o *Config) SetDefaults() {
if o.SupportSettings.ReportAProblemLink == nil {
o.SupportSettings.ReportAProblemLink = new(string)
*o.SupportSettings.ReportAProblemLink = "/static/help/report_problem.html"
*o.SupportSettings.ReportAProblemLink = ""
}
if o.SupportSettings.SupportEmail == nil {
@ -675,6 +738,20 @@ func (o *Config) SetDefaults() {
*o.ServiceSettings.RestrictCustomEmojiCreation = RESTRICT_EMOJI_CREATION_ALL
}
if o.ClusterSettings.InterNodeListenAddress == nil {
o.ClusterSettings.InterNodeListenAddress = new(string)
*o.ClusterSettings.InterNodeListenAddress = ":8075"
}
if o.ClusterSettings.Enable == nil {
o.ClusterSettings.Enable = new(bool)
*o.ClusterSettings.Enable = false
}
if o.ClusterSettings.InterNodeUrls == nil {
o.ClusterSettings.InterNodeUrls = []string{}
}
if o.ComplianceSettings.Enable == nil {
o.ComplianceSettings.Enable = new(bool)
*o.ComplianceSettings.Enable = false
@ -705,6 +782,11 @@ func (o *Config) SetDefaults() {
*o.LocalizationSettings.AvailableLocales = ""
}
if o.LogSettings.EnableDiagnostics == nil {
o.LogSettings.EnableDiagnostics = new(bool)
*o.LogSettings.EnableDiagnostics = true
}
if o.SamlSettings.Enable == nil {
o.SamlSettings.Enable = new(bool)
*o.SamlSettings.Enable = false
@ -784,6 +866,21 @@ func (o *Config) SetDefaults() {
o.SamlSettings.LocaleAttribute = new(string)
*o.SamlSettings.LocaleAttribute = ""
}
if o.NativeAppSettings.AppDownloadLink == nil {
o.NativeAppSettings.AppDownloadLink = new(string)
*o.NativeAppSettings.AppDownloadLink = "https://about.mattermost.com/downloads/"
}
if o.NativeAppSettings.AndroidAppDownloadLink == nil {
o.NativeAppSettings.AndroidAppDownloadLink = new(string)
*o.NativeAppSettings.AndroidAppDownloadLink = "https://about.mattermost.com/mattermost-android-app/"
}
if o.NativeAppSettings.IosAppDownloadLink == nil {
o.NativeAppSettings.IosAppDownloadLink = new(string)
*o.NativeAppSettings.IosAppDownloadLink = "https://about.mattermost.com/mattermost-ios-app/"
}
}
func (o *Config) IsValid() *AppError {
@ -792,10 +889,24 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.login_attempts.app_error", nil, "")
}
if len(*o.ServiceSettings.SiteURL) != 0 {
if _, err := url.ParseRequestURI(*o.ServiceSettings.SiteURL); err != nil {
return NewLocAppError("Config.IsValid", "model.config.is_valid.site_url.app_error", nil, "")
}
}
if len(o.ServiceSettings.ListenAddress) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.listen_address.app_error", nil, "")
}
if *o.ClusterSettings.Enable && *o.EmailSettings.EnableEmailBatching {
return NewLocAppError("Config.IsValid", "model.config.is_valid.cluster_email_batching.app_error", nil, "")
}
if len(*o.ServiceSettings.SiteURL) == 0 && *o.EmailSettings.EnableEmailBatching {
return NewLocAppError("Config.IsValid", "model.config.is_valid.site_url_email_batching.app_error", nil, "")
}
if o.TeamSettings.MaxUsersPerTeam <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.max_users.app_error", nil, "")
}
@ -856,11 +967,11 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.file_thumb_width.app_error", nil, "")
}
if len(o.FileSettings.PublicLinkSalt) < 32 {
if len(*o.FileSettings.PublicLinkSalt) < 32 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.file_salt.app_error", nil, "")
}
if !(o.EmailSettings.ConnectionSecurity == CONN_SECURITY_NONE || o.EmailSettings.ConnectionSecurity == CONN_SECURITY_TLS || o.EmailSettings.ConnectionSecurity == CONN_SECURITY_STARTTLS) {
if !(o.EmailSettings.ConnectionSecurity == CONN_SECURITY_NONE || o.EmailSettings.ConnectionSecurity == CONN_SECURITY_TLS || o.EmailSettings.ConnectionSecurity == CONN_SECURITY_STARTTLS || o.EmailSettings.ConnectionSecurity == CONN_SECURITY_PLAIN) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.email_security.app_error", nil, "")
}
@ -872,6 +983,14 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.email_reset_salt.app_error", nil, "")
}
if *o.EmailSettings.EmailBatchingBufferSize <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.email_batching_buffer_size.app_error", nil, "")
}
if *o.EmailSettings.EmailBatchingInterval < 30 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.email_batching_interval.app_error", nil, "")
}
if o.RateLimitSettings.MemoryStoreSize <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.rate_mem.app_error", nil, "")
}
@ -893,21 +1012,29 @@ func (o *Config) IsValid() *AppError {
}
if *o.LdapSettings.Enable {
if *o.LdapSettings.LdapServer == "" ||
*o.LdapSettings.BaseDN == "" ||
*o.LdapSettings.BindUsername == "" ||
*o.LdapSettings.BindPassword == "" ||
*o.LdapSettings.FirstNameAttribute == "" ||
*o.LdapSettings.LastNameAttribute == "" ||
*o.LdapSettings.EmailAttribute == "" ||
*o.LdapSettings.UsernameAttribute == "" ||
*o.LdapSettings.IdAttribute == "" {
return NewLocAppError("Config.IsValid", "Required LDAP field missing", nil, "")
if *o.LdapSettings.LdapServer == "" {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_server", nil, "")
}
if *o.LdapSettings.BaseDN == "" {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_basedn", nil, "")
}
if *o.LdapSettings.EmailAttribute == "" {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_email", nil, "")
}
if *o.LdapSettings.UsernameAttribute == "" {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_username", nil, "")
}
if *o.LdapSettings.IdAttribute == "" {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_id", nil, "")
}
}
if *o.SamlSettings.Enable {
if len(*o.SamlSettings.IdpUrl) == 0 {
if len(*o.SamlSettings.IdpUrl) == 0 || !IsValidHttpUrl(*o.SamlSettings.IdpUrl) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_idp_url.app_error", nil, "")
}
@ -927,14 +1054,6 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_username_attribute.app_error", nil, "")
}
if len(*o.SamlSettings.FirstNameAttribute) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_first_name_attribute.app_error", nil, "")
}
if len(*o.SamlSettings.LastNameAttribute) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_last_name_attribute.app_error", nil, "")
}
if *o.SamlSettings.Verify {
if len(*o.SamlSettings.AssertionConsumerServiceURL) == 0 || !IsValidHttpUrl(*o.SamlSettings.AssertionConsumerServiceURL) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_assertion_consumer_service_url.app_error", nil, "")
@ -960,6 +1079,10 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.password_length.app_error", map[string]interface{}{"MinLength": PASSWORD_MINIMUM_LENGTH, "MaxLength": PASSWORD_MAXIMUM_LENGTH}, "")
}
if len(o.TeamSettings.SiteName) > SITENAME_MAX_LENGTH {
return NewLocAppError("Config.IsValid", "model.config.is_valid.sitename_length.app_error", map[string]interface{}{"MaxLength": SITENAME_MAX_LENGTH}, "")
}
return nil
}
@ -976,7 +1099,7 @@ func (o *Config) Sanitize() {
*o.LdapSettings.BindPassword = FAKE_SETTING
}
o.FileSettings.PublicLinkSalt = FAKE_SETTING
*o.FileSettings.PublicLinkSalt = FAKE_SETTING
if len(o.FileSettings.AmazonS3SecretAccessKey) > 0 {
o.FileSettings.AmazonS3SecretAccessKey = FAKE_SETTING
}

View File

@ -84,6 +84,12 @@ func (task *ScheduledTask) Cancel() {
removeTaskByName(task.Name)
}
// Executes the task immediatly. A recurring task will be run regularally after interval.
func (task *ScheduledTask) Execute() {
task.function()
task.timer.Reset(task.Interval)
}
func (task *ScheduledTask) String() string {
return fmt.Sprintf(
"%s\nInterval: %s\nRecurring: %t\n",

View File

@ -35,8 +35,10 @@ type Features struct {
Users *int `json:"users"`
LDAP *bool `json:"ldap"`
MFA *bool `json:"mfa"`
GoogleSSO *bool `json:"google_sso"`
GoogleOAuth *bool `json:"google_oauth"`
Office365OAuth *bool `json:"office365_oauth"`
Compliance *bool `json:"compliance"`
Cluster *bool `json:"cluster"`
CustomBrand *bool `json:"custom_brand"`
MHPNS *bool `json:"mhpns"`
SAML *bool `json:"saml"`
@ -44,6 +46,22 @@ type Features struct {
FutureFeatures *bool `json:"future_features"`
}
func (f *Features) ToMap() map[string]interface{} {
return map[string]interface{}{
"ldap": *f.LDAP,
"mfa": *f.MFA,
"google": *f.GoogleOAuth,
"office365": *f.Office365OAuth,
"compliance": *f.Compliance,
"cluster": *f.Cluster,
"custom_brand": *f.CustomBrand,
"mhpns": *f.MHPNS,
"saml": *f.SAML,
"password": *f.PasswordRequirements,
"future": *f.FutureFeatures,
}
}
func (f *Features) SetDefaults() {
if f.FutureFeatures == nil {
f.FutureFeatures = new(bool)
@ -65,9 +83,14 @@ func (f *Features) SetDefaults() {
*f.MFA = *f.FutureFeatures
}
if f.GoogleSSO == nil {
f.GoogleSSO = new(bool)
*f.GoogleSSO = *f.FutureFeatures
if f.GoogleOAuth == nil {
f.GoogleOAuth = new(bool)
*f.GoogleOAuth = *f.FutureFeatures
}
if f.Office365OAuth == nil {
f.Office365OAuth = new(bool)
*f.Office365OAuth = *f.FutureFeatures
}
if f.Compliance == nil {
@ -75,6 +98,11 @@ func (f *Features) SetDefaults() {
*f.Compliance = *f.FutureFeatures
}
if f.Cluster == nil {
f.Cluster = new(bool)
*f.Cluster = *f.FutureFeatures
}
if f.CustomBrand == nil {
f.CustomBrand = new(bool)
*f.CustomBrand = *f.FutureFeatures

View File

@ -1,61 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
ACTION_TYPING = "typing"
ACTION_POSTED = "posted"
ACTION_POST_EDITED = "post_edited"
ACTION_POST_DELETED = "post_deleted"
ACTION_CHANNEL_DELETED = "channel_deleted"
ACTION_CHANNEL_VIEWED = "channel_viewed"
ACTION_DIRECT_ADDED = "direct_added"
ACTION_NEW_USER = "new_user"
ACTION_LEAVE_TEAM = "leave_team"
ACTION_USER_ADDED = "user_added"
ACTION_USER_REMOVED = "user_removed"
ACTION_PREFERENCE_CHANGED = "preference_changed"
ACTION_EPHEMERAL_MESSAGE = "ephemeral_message"
)
type Message struct {
TeamId string `json:"team_id"`
ChannelId string `json:"channel_id"`
UserId string `json:"user_id"`
Action string `json:"action"`
Props map[string]string `json:"props"`
}
func (m *Message) Add(key string, value string) {
m.Props[key] = value
}
func NewMessage(teamId string, channelId string, userId string, action string) *Message {
return &Message{TeamId: teamId, ChannelId: channelId, UserId: userId, Action: action, Props: make(map[string]string)}
}
func (o *Message) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func MessageFromJson(data io.Reader) *Message {
decoder := json.NewDecoder(data)
var o Message
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -25,8 +25,10 @@ type OAuthApp struct {
ClientSecret string `json:"client_secret"`
Name string `json:"name"`
Description string `json:"description"`
IconURL string `json:"icon_url"`
CallbackUrls StringArray `json:"callback_urls"`
Homepage string `json:"homepage"`
IsTrusted bool `json:"is_trusted"`
}
// IsValid validates the app and returns an error if it isn't configured
@ -61,7 +63,13 @@ func (a *OAuthApp) IsValid() *AppError {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "app_id="+a.Id)
}
if len(a.Homepage) == 0 || len(a.Homepage) > 256 {
for _, callback := range a.CallbackUrls {
if !IsValidHttpUrl(callback) {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "")
}
}
if len(a.Homepage) == 0 || len(a.Homepage) > 256 || !IsValidHttpUrl(a.Homepage) {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.homepage.app_error", nil, "app_id="+a.Id)
}
@ -69,6 +77,12 @@ func (a *OAuthApp) IsValid() *AppError {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.description.app_error", nil, "app_id="+a.Id)
}
if len(a.IconURL) > 0 {
if len(a.IconURL) > 512 || !IsValidHttpUrl(a.IconURL) {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.icon_url.app_error", nil, "app_id="+a.Id)
}
}
return nil
}
@ -85,10 +99,6 @@ func (a *OAuthApp) PreSave() {
a.CreateAt = GetMillis()
a.UpdateAt = a.CreateAt
if len(a.ClientSecret) > 0 {
a.ClientSecret = HashPassword(a.ClientSecret)
}
}
// PreUpdate should be run before updating the app in the db.
@ -157,3 +167,23 @@ func OAuthAppMapFromJson(data io.Reader) map[string]*OAuthApp {
return nil
}
}
func OAuthAppListToJson(l []*OAuthApp) string {
b, err := json.Marshal(l)
if err != nil {
return ""
} else {
return string(b)
}
}
func OAuthAppListFromJson(data io.Reader) []*OAuthApp {
decoder := json.NewDecoder(data)
var o []*OAuthApp
err := decoder.Decode(&o)
if err == nil {
return o
} else {
return nil
}
}

View File

@ -9,6 +9,7 @@ import (
"io"
"net/url"
"strconv"
"strings"
)
type OutgoingWebhook struct {
@ -21,6 +22,7 @@ type OutgoingWebhook struct {
ChannelId string `json:"channel_id"`
TeamId string `json:"team_id"`
TriggerWords StringArray `json:"trigger_words"`
TriggerWhen int `json:"trigger_when"`
CallbackURLs StringArray `json:"callback_urls"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
@ -171,6 +173,10 @@ func (o *OutgoingWebhook) IsValid() *AppError {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "")
}
if o.TriggerWhen > 1 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "")
}
return nil
}
@ -204,3 +210,17 @@ func (o *OutgoingWebhook) HasTriggerWord(word string) bool {
return false
}
func (o *OutgoingWebhook) TriggerWordStartsWith(word string) bool {
if len(o.TriggerWords) == 0 || len(word) == 0 {
return false
}
for _, trigger := range o.TriggerWords {
if strings.HasPrefix(word, trigger) {
return true
}
}
return false
}

View File

@ -15,6 +15,7 @@ const (
POST_SLACK_ATTACHMENT = "slack_attachment"
POST_SYSTEM_GENERIC = "system_generic"
POST_JOIN_LEAVE = "system_join_leave"
POST_ADD_REMOVE = "system_add_remove"
POST_HEADER_CHANGE = "system_header_change"
POST_CHANNEL_DELETED = "system_channel_deleted"
POST_EPHEMERAL = "system_ephemeral"
@ -109,7 +110,7 @@ func (o *Post) IsValid() *AppError {
}
// should be removed once more message types are supported
if !(o.Type == POST_DEFAULT || o.Type == POST_JOIN_LEAVE || o.Type == POST_SLACK_ATTACHMENT || o.Type == POST_HEADER_CHANGE) {
if !(o.Type == POST_DEFAULT || o.Type == POST_JOIN_LEAVE || o.Type == POST_ADD_REMOVE || o.Type == POST_SLACK_ATTACHMENT || o.Type == POST_HEADER_CHANGE) {
return NewLocAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type)
}
@ -162,9 +163,6 @@ func (o *Post) AddProp(key string, value interface{}) {
o.Props[key] = value
}
func (o *Post) PreExport() {
}
func (o *Post) IsSystemMessage() bool {
return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX
}

View File

@ -6,6 +6,8 @@ package model
import (
"encoding/json"
"io"
"regexp"
"strings"
"unicode/utf8"
)
@ -13,12 +15,28 @@ const (
PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW = "direct_channel_show"
PREFERENCE_CATEGORY_TUTORIAL_STEPS = "tutorial_step"
PREFERENCE_CATEGORY_ADVANCED_SETTINGS = "advanced_settings"
PREFERENCE_CATEGORY_FLAGGED_POST = "flagged_post"
PREFERENCE_CATEGORY_DISPLAY_SETTINGS = "display_settings"
PREFERENCE_NAME_COLLAPSE_SETTING = "collapse_previews"
PREFERENCE_CATEGORY_DISPLAY_SETTINGS = "display_settings"
PREFERENCE_NAME_COLLAPSE_SETTING = "collapse_previews"
PREFERENCE_NAME_DISPLAY_NAME_FORMAT = "name_format"
PREFERENCE_VALUE_DISPLAY_NAME_NICKNAME = "nickname_full_name"
PREFERENCE_VALUE_DISPLAY_NAME_FULL = "full_name"
PREFERENCE_VALUE_DISPLAY_NAME_USERNAME = "username"
PREFERENCE_DEFAULT_DISPLAY_NAME_FORMAT = PREFERENCE_VALUE_DISPLAY_NAME_USERNAME
PREFERENCE_CATEGORY_THEME = "theme"
// the name for theme props is the team id
PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP = "oauth_app"
// the name for oauth_app is the client_id and value is the current scope
PREFERENCE_CATEGORY_LAST = "last"
PREFERENCE_NAME_LAST_CHANNEL = "channel"
PREFERENCE_CATEGORY_NOTIFICATIONS = "notifications"
PREFERENCE_NAME_EMAIL_INTERVAL = "email_interval"
PREFERENCE_DEFAULT_EMAIL_INTERVAL = "30" // default to match the interval of the "immediate" setting (ie 30 seconds)
)
type Preference struct {
@ -57,13 +75,48 @@ func (o *Preference) IsValid() *AppError {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.category.app_error", nil, "category="+o.Category)
}
if len(o.Name) == 0 || len(o.Name) > 32 {
if len(o.Name) > 32 {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.name.app_error", nil, "name="+o.Name)
}
if utf8.RuneCountInString(o.Value) > 128 {
if utf8.RuneCountInString(o.Value) > 2000 {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.value.app_error", nil, "value="+o.Value)
}
if o.Category == PREFERENCE_CATEGORY_THEME {
var unused map[string]string
if err := json.NewDecoder(strings.NewReader(o.Value)).Decode(&unused); err != nil {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.theme.app_error", nil, "value="+o.Value)
}
}
return nil
}
func (o *Preference) PreUpdate() {
if o.Category == PREFERENCE_CATEGORY_THEME {
// decode the value of theme (a map of strings to string) and eliminate any invalid values
var props map[string]string
if err := json.NewDecoder(strings.NewReader(o.Value)).Decode(&props); err != nil {
// just continue, the invalid preference value should get caught by IsValid before saving
return
}
colorPattern := regexp.MustCompile(`^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$`)
// blank out any invalid theme values
for name, value := range props {
if name == "image" || name == "type" || name == "codeTheme" {
continue
}
if !colorPattern.MatchString(value) {
props[name] = "#ffffff"
}
}
if b, err := json.Marshal(props); err == nil {
o.Value = string(b)
}
}
}

View File

@ -6,12 +6,16 @@ package model
import (
"encoding/json"
"io"
"strings"
)
const (
PUSH_NOTIFY_APPLE = "apple"
PUSH_NOTIFY_ANDROID = "android"
PUSH_TYPE_MESSAGE = "message"
PUSH_TYPE_CLEAR = "clear"
CATEGORY_DM = "DIRECT_MESSAGE"
MHPNS = "https://push.mattermost.com"
@ -28,6 +32,7 @@ type PushNotification struct {
ContentAvailable int `json:"cont_ava"`
ChannelId string `json:"channel_id"`
ChannelName string `json:"channel_name"`
Type string `json:"type"`
}
func (me *PushNotification) ToJson() string {
@ -39,6 +44,16 @@ func (me *PushNotification) ToJson() string {
}
}
func (me *PushNotification) SetDeviceIdAndPlatform(deviceId string) {
if strings.HasPrefix(deviceId, PUSH_NOTIFY_APPLE+":") {
me.Platform = PUSH_NOTIFY_APPLE
me.DeviceId = strings.TrimPrefix(deviceId, PUSH_NOTIFY_APPLE+":")
} else if strings.HasPrefix(deviceId, PUSH_NOTIFY_ANDROID+":") {
me.Platform = PUSH_NOTIFY_ANDROID
me.DeviceId = strings.TrimPrefix(deviceId, PUSH_NOTIFY_ANDROID+":")
}
}
func PushNotificationFromJson(data io.Reader) *PushNotification {
decoder := json.NewDecoder(data)
var me PushNotification

View File

@ -6,6 +6,7 @@ package model
import (
"encoding/json"
"io"
"strings"
)
const (
@ -83,7 +84,11 @@ func (me *Session) IsExpired() bool {
}
func (me *Session) SetExpireInDays(days int) {
me.ExpiresAt = GetMillis() + (1000 * 60 * 60 * 24 * int64(days))
if me.CreateAt == 0 {
me.ExpiresAt = GetMillis() + (1000 * 60 * 60 * 24 * int64(days))
} else {
me.ExpiresAt = me.CreateAt + (1000 * 60 * 60 * 24 * int64(days))
}
}
func (me *Session) AddProp(key string, value string) {
@ -105,6 +110,11 @@ func (me *Session) GetTeamByTeamId(teamId string) *TeamMember {
return nil
}
func (me *Session) IsMobileApp() bool {
return len(me.DeviceId) > 0 &&
(strings.HasPrefix(me.DeviceId, PUSH_NOTIFY_APPLE+":") || strings.HasPrefix(me.DeviceId, PUSH_NOTIFY_ANDROID+":"))
}
func SessionsToJson(o []*Session) string {
if b, err := json.Marshal(o); err != nil {
return "[]"

45
vendor/github.com/mattermost/platform/model/status.go generated vendored Normal file
View File

@ -0,0 +1,45 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
STATUS_OFFLINE = "offline"
STATUS_AWAY = "away"
STATUS_ONLINE = "online"
STATUS_CACHE_SIZE = 10000
STATUS_CHANNEL_TIMEOUT = 20000 // 20 seconds
)
type Status struct {
UserId string `json:"user_id"`
Status string `json:"status"`
Manual bool `json:"manual"`
LastActivityAt int64 `json:"last_activity_at"`
ActiveChannel string `json:"active_channel"`
}
func (o *Status) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func StatusFromJson(data io.Reader) *Status {
decoder := json.NewDecoder(data)
var o Status
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -224,9 +224,6 @@ func CleanTeamName(s string) string {
return s
}
func (o *Team) PreExport() {
}
func (o *Team) Sanitize() {
o.Email = ""
o.AllowedDomains = ""

View File

@ -16,11 +16,6 @@ import (
const (
ROLE_SYSTEM_ADMIN = "system_admin"
USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute
USER_OFFLINE = "offline"
USER_AWAY = "away"
USER_ONLINE = "online"
USER_NOTIFY_ALL = "all"
USER_NOTIFY_MENTION = "mention"
USER_NOTIFY_NONE = "none"
@ -44,18 +39,16 @@ type User struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Roles string `json:"roles"`
LastActivityAt int64 `json:"last_activity_at,omitempty"`
LastPingAt int64 `json:"last_ping_at,omitempty"`
AllowMarketing bool `json:"allow_marketing,omitempty"`
Props StringMap `json:"props,omitempty"`
NotifyProps StringMap `json:"notify_props,omitempty"`
ThemeProps StringMap `json:"theme_props,omitempty"`
LastPasswordUpdate int64 `json:"last_password_update,omitempty"`
LastPictureUpdate int64 `json:"last_picture_update,omitempty"`
FailedAttempts int `json:"failed_attempts,omitempty"`
Locale string `json:"locale"`
MfaActive bool `json:"mfa_active,omitempty"`
MfaSecret string `json:"mfa_secret,omitempty"`
LastActivityAt int64 `db:"-" json:"last_activity_at,omitempty"`
}
// IsValid validates the user and returns an error if it isn't configured
@ -106,10 +99,6 @@ func (u *User) IsValid() *AppError {
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_pwd.app_error", nil, "user_id="+u.Id)
}
if len(u.ThemeProps) > 2000 {
return NewLocAppError("User.IsValid", "model.user.is_valid.theme.app_error", nil, "user_id="+u.Id)
}
return nil
}
@ -179,21 +168,6 @@ func (u *User) PreUpdate() {
}
u.NotifyProps["mention_keys"] = strings.Join(goodKeys, ",")
}
if u.ThemeProps != nil {
colorPattern := regexp.MustCompile(`^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$`)
// blank out any invalid theme values
for name, value := range u.ThemeProps {
if name == "image" || name == "type" || name == "codeTheme" {
continue
}
if !colorPattern.MatchString(value) {
u.ThemeProps[name] = "#ffffff"
}
}
}
}
func (u *User) SetDefaultNotifications() {
@ -242,14 +216,6 @@ func (u *User) Etag(showFullName, showEmail bool) string {
return Etag(u.Id, u.UpdateAt, showFullName, showEmail)
}
func (u *User) IsOffline() bool {
return (GetMillis()-u.LastPingAt) > USER_OFFLINE_TIMEOUT && (GetMillis()-u.LastActivityAt) > USER_OFFLINE_TIMEOUT
}
func (u *User) IsAway() bool {
return (GetMillis() - u.LastActivityAt) > USER_AWAY_TIMEOUT
}
// Remove any private data from the user object
func (u *User) Sanitize(options map[string]bool) {
u.Password = ""
@ -270,7 +236,6 @@ func (u *User) Sanitize(options map[string]bool) {
}
func (u *User) ClearNonProfileFields() {
u.UpdateAt = 0
u.Password = ""
u.AuthData = new(string)
*u.AuthData = ""
@ -278,16 +243,20 @@ func (u *User) ClearNonProfileFields() {
u.MfaActive = false
u.MfaSecret = ""
u.EmailVerified = false
u.LastPingAt = 0
u.AllowMarketing = false
u.Props = StringMap{}
u.NotifyProps = StringMap{}
u.ThemeProps = StringMap{}
u.LastPasswordUpdate = 0
u.LastPictureUpdate = 0
u.FailedAttempts = 0
}
func (u *User) SanitizeProfile(options map[string]bool) {
u.ClearNonProfileFields()
u.Sanitize(options)
}
func (u *User) MakeNonNil() {
if u.Props == nil {
u.Props = make(map[string]string)
@ -332,6 +301,24 @@ func (u *User) GetDisplayName() string {
}
}
func (u *User) GetDisplayNameForPreference(nameFormat string) string {
displayName := u.Username
if nameFormat == PREFERENCE_VALUE_DISPLAY_NAME_NICKNAME {
if u.Nickname != "" {
displayName = u.Nickname
} else if fullName := u.GetFullName(); fullName != "" {
displayName = fullName
}
} else if nameFormat == PREFERENCE_VALUE_DISPLAY_NAME_FULL {
if fullName := u.GetFullName(); fullName != "" {
displayName = fullName
}
}
return displayName
}
func IsValidUserRoles(userRoles string) bool {
roles := strings.Split(userRoles, " ")
@ -392,17 +379,6 @@ func (u *User) IsLDAPUser() bool {
return false
}
func (u *User) PreExport() {
u.Password = ""
u.AuthData = new(string)
*u.AuthData = ""
u.LastActivityAt = 0
u.LastPingAt = 0
u.LastPasswordUpdate = 0
u.LastPictureUpdate = 0
u.FailedAttempts = 0
}
// UserFromJson will decode the input and return a User
func UserFromJson(data io.Reader) *User {
decoder := json.NewDecoder(data)
@ -461,6 +437,7 @@ var validUsernameChars = regexp.MustCompile(`^[a-z0-9\.\-_]+$`)
var restrictedUsernames = []string{
"all",
"channel",
"matterbot",
}
func IsValidUsername(s string) bool {

View File

@ -34,12 +34,12 @@ type EncryptStringMap map[string]string
type AppError struct {
Id string `json:"id"`
Message string `json:"message"` // Message to be display to the end user without debugging information
DetailedError string `json:"detailed_error"` // Internal error string to help the developer
RequestId string `json:"request_id"` // The RequestId that's also set in the header
StatusCode int `json:"status_code"` // The http status code
Where string `json:"-"` // The function where it happened in the form of Struct.Func
IsOAuth bool `json:"is_oauth"` // Whether the error is OAuth specific
Message string `json:"message"` // Message to be display to the end user without debugging information
DetailedError string `json:"detailed_error"` // Internal error string to help the developer
RequestId string `json:"request_id,omitempty"` // The RequestId that's also set in the header
StatusCode int `json:"status_code,omitempty"` // The http status code
Where string `json:"-"` // The function where it happened in the form of Struct.Func
IsOAuth bool `json:"is_oauth,omitempty"` // Whether the error is OAuth specific
params map[string]interface{} `json:"-"`
}

View File

@ -13,6 +13,8 @@ import (
// It should be maitained in chronological order with most current
// release at the front of the list.
var versions = []string{
"3.4.0",
"3.3.0",
"3.2.0",
"3.1.0",
"3.0.0",

View File

@ -0,0 +1,109 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"github.com/gorilla/websocket"
"net/http"
)
type WebSocketClient struct {
Url string // The location of the server like "ws://localhost:8065"
ApiUrl string // The api location of the server like "ws://localhost:8065/api/v3"
Conn *websocket.Conn // The WebSocket connection
AuthToken string // The token used to open the WebSocket
Sequence int64 // The ever-incrementing sequence attached to each WebSocket action
EventChannel chan *WebSocketEvent
ResponseChannel chan *WebSocketResponse
}
// NewWebSocketClient constructs a new WebSocket client with convienence
// methods for talking to the server.
func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) {
header := http.Header{}
header.Set(HEADER_AUTH, "BEARER "+authToken)
conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX+"/users/websocket", header)
if err != nil {
return nil, NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
}
return &WebSocketClient{
url,
url + API_URL_SUFFIX,
conn,
authToken,
1,
make(chan *WebSocketEvent, 100),
make(chan *WebSocketResponse, 100),
}, nil
}
func (wsc *WebSocketClient) Connect() *AppError {
header := http.Header{}
header.Set(HEADER_AUTH, "BEARER "+wsc.AuthToken)
var err error
wsc.Conn, _, err = websocket.DefaultDialer.Dial(wsc.ApiUrl+"/users/websocket", header)
if err != nil {
return NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
}
return nil
}
func (wsc *WebSocketClient) Close() {
wsc.Conn.Close()
}
func (wsc *WebSocketClient) Listen() {
go func() {
for {
var rawMsg json.RawMessage
var err error
if _, rawMsg, err = wsc.Conn.ReadMessage(); err != nil {
return
}
var event WebSocketEvent
if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() {
wsc.EventChannel <- &event
continue
}
var response WebSocketResponse
if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
wsc.ResponseChannel <- &response
continue
}
}
}()
}
func (wsc *WebSocketClient) SendMessage(action string, data map[string]interface{}) {
req := &WebSocketRequest{}
req.Seq = wsc.Sequence
req.Action = action
req.Data = data
wsc.Sequence++
wsc.Conn.WriteJSON(req)
}
// UserTyping will push a user_typing event out to all connected users
// who are in the specified channel
func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
data := map[string]interface{}{
"channel_id": channelId,
"parent_id": parentId,
}
wsc.SendMessage("user_typing", data)
}
// GetStatuses will return a map of string statuses using user id as the key
func (wsc *WebSocketClient) GetStatuses() {
wsc.SendMessage("get_statuses", nil)
}

View File

@ -0,0 +1,116 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
WEBSOCKET_EVENT_TYPING = "typing"
WEBSOCKET_EVENT_POSTED = "posted"
WEBSOCKET_EVENT_POST_EDITED = "post_edited"
WEBSOCKET_EVENT_POST_DELETED = "post_deleted"
WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted"
WEBSOCKET_EVENT_CHANNEL_VIEWED = "channel_viewed"
WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added"
WEBSOCKET_EVENT_NEW_USER = "new_user"
WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team"
WEBSOCKET_EVENT_USER_ADDED = "user_added"
WEBSOCKET_EVENT_USER_UPDATED = "user_updated"
WEBSOCKET_EVENT_USER_REMOVED = "user_removed"
WEBSOCKET_EVENT_PREFERENCE_CHANGED = "preference_changed"
WEBSOCKET_EVENT_EPHEMERAL_MESSAGE = "ephemeral_message"
WEBSOCKET_EVENT_STATUS_CHANGE = "status_change"
WEBSOCKET_EVENT_HELLO = "hello"
)
type WebSocketMessage interface {
ToJson() string
IsValid() bool
}
type WebSocketEvent struct {
TeamId string `json:"team_id"`
ChannelId string `json:"channel_id"`
UserId string `json:"user_id"`
Event string `json:"event"`
Data map[string]interface{} `json:"data"`
}
func (m *WebSocketEvent) Add(key string, value interface{}) {
m.Data[key] = value
}
func NewWebSocketEvent(teamId string, channelId string, userId string, event string) *WebSocketEvent {
return &WebSocketEvent{TeamId: teamId, ChannelId: channelId, UserId: userId, Event: event, Data: make(map[string]interface{})}
}
func (o *WebSocketEvent) IsValid() bool {
return o.Event != ""
}
func (o *WebSocketEvent) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func WebSocketEventFromJson(data io.Reader) *WebSocketEvent {
decoder := json.NewDecoder(data)
var o WebSocketEvent
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}
type WebSocketResponse struct {
Status string `json:"status"`
SeqReply int64 `json:"seq_reply,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
Error *AppError `json:"error,omitempty"`
}
func (m *WebSocketResponse) Add(key string, value interface{}) {
m.Data[key] = value
}
func NewWebSocketResponse(status string, seqReply int64, data map[string]interface{}) *WebSocketResponse {
return &WebSocketResponse{Status: status, SeqReply: seqReply, Data: data}
}
func NewWebSocketError(seqReply int64, err *AppError) *WebSocketResponse {
return &WebSocketResponse{Status: STATUS_FAIL, SeqReply: seqReply, Error: err}
}
func (o *WebSocketResponse) IsValid() bool {
return o.Status != ""
}
func (o *WebSocketResponse) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func WebSocketResponseFromJson(data io.Reader) *WebSocketResponse {
decoder := json.NewDecoder(data)
var o WebSocketResponse
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
goi18n "github.com/nicksnyder/go-i18n/i18n"
)
type WebSocketRequest struct {
// Client-provided fields
Seq int64 `json:"seq"`
Action string `json:"action"`
Data map[string]interface{} `json:"data"`
// Server-provided fields
Session Session `json:"-"`
T goi18n.TranslateFunc `json:"-"`
Locale string `json:"-"`
}
func (o *WebSocketRequest) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func WebSocketRequestFromJson(data io.Reader) *WebSocketRequest {
decoder := json.NewDecoder(data)
var o WebSocketRequest
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

27
vendor/github.com/mattn/go-xmpp/LICENSE generated vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. 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 Google Inc. 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
OWNER 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.

881
vendor/github.com/mattn/go-xmpp/xmpp.go generated vendored Normal file
View File

@ -0,0 +1,881 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// TODO(rsc):
// More precise error handling.
// Presence functionality.
// TODO(mattn):
// Add proxy authentication.
// Package xmpp implements a simple Google Talk client
// using the XMPP protocol described in RFC 3920 and RFC 3921.
package xmpp
import (
"bufio"
"bytes"
"crypto/md5"
"crypto/rand"
"crypto/tls"
"encoding/base64"
"encoding/binary"
"encoding/xml"
"errors"
"fmt"
"io"
"math/big"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
)
const (
nsStream = "http://etherx.jabber.org/streams"
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
nsClient = "jabber:client"
nsSession = "urn:ietf:params:xml:ns:xmpp-session"
)
// Default TLS configuration options
var DefaultConfig tls.Config
// Cookie is a unique XMPP session identifier
type Cookie uint64
func getCookie() Cookie {
var buf [8]byte
if _, err := rand.Reader.Read(buf[:]); err != nil {
panic("Failed to read random bytes: " + err.Error())
}
return Cookie(binary.LittleEndian.Uint64(buf[:]))
}
// Client holds XMPP connection opitons
type Client struct {
conn net.Conn // connection to server
jid string // Jabber ID for our connection
domain string
p *xml.Decoder
}
func (c *Client) JID() string {
return c.jid
}
func connect(host, user, passwd string) (net.Conn, error) {
addr := host
if strings.TrimSpace(host) == "" {
a := strings.SplitN(user, "@", 2)
if len(a) == 2 {
addr = a[1]
}
}
a := strings.SplitN(host, ":", 2)
if len(a) == 1 {
addr += ":5222"
}
proxy := os.Getenv("HTTP_PROXY")
if proxy == "" {
proxy = os.Getenv("http_proxy")
}
if proxy != "" {
url, err := url.Parse(proxy)
if err == nil {
addr = url.Host
}
}
c, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
if proxy != "" {
fmt.Fprintf(c, "CONNECT %s HTTP/1.1\r\n", host)
fmt.Fprintf(c, "Host: %s\r\n", host)
fmt.Fprintf(c, "\r\n")
br := bufio.NewReader(c)
req, _ := http.NewRequest("CONNECT", host, nil)
resp, err := http.ReadResponse(br, req)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
f := strings.SplitN(resp.Status, " ", 2)
return nil, errors.New(f[1])
}
}
return c, nil
}
// Options are used to specify additional options for new clients, such as a Resource.
type Options struct {
// Host specifies what host to connect to, as either "hostname" or "hostname:port"
// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
// Default the port to 5222.
Host string
// User specifies what user to authenticate to the remote server.
User string
// Password supplies the password to use for authentication with the remote server.
Password string
// Resource specifies an XMPP client resource, like "bot", instead of accepting one
// from the server. Use "" to let the server generate one for your client.
Resource string
// OAuthScope provides go-xmpp the required scope for OAuth2 authentication.
OAuthScope string
// OAuthToken provides go-xmpp with the required OAuth2 token used to authenticate
OAuthToken string
// OAuthXmlNs provides go-xmpp with the required namespaced used for OAuth2 authentication. This is
// provided to the server as the xmlns:auth attribute of the OAuth2 authentication request.
OAuthXmlNs string
// TLS Config
TLSConfig *tls.Config
// InsecureAllowUnencryptedAuth permits authentication over a TCP connection that has not been promoted to
// TLS by STARTTLS; this could leak authentication information over the network, or permit man in the middle
// attacks.
InsecureAllowUnencryptedAuth bool
// NoTLS directs go-xmpp to not use TLS initially to contact the server; instead, a plain old unencrypted
// TCP connection should be used. (Can be combined with StartTLS to support STARTTLS-based servers.)
NoTLS bool
// StartTLS directs go-xmpp to STARTTLS if the server supports it; go-xmpp will automatically STARTTLS
// if the server requires it regardless of this option.
StartTLS bool
// Debug output
Debug bool
// Use server sessions
Session bool
// Presence Status
Status string
// Status message
StatusMessage string
}
// NewClient establishes a new Client connection based on a set of Options.
func (o Options) NewClient() (*Client, error) {
host := o.Host
c, err := connect(host, o.User, o.Password)
if err != nil {
return nil, err
}
if strings.LastIndex(o.Host, ":") > 0 {
host = host[:strings.LastIndex(o.Host, ":")]
}
client := new(Client)
if o.NoTLS {
client.conn = c
} else {
var tlsconn *tls.Conn
if o.TLSConfig != nil {
tlsconn = tls.Client(c, o.TLSConfig)
} else {
DefaultConfig.ServerName = host
tlsconn = tls.Client(c, &DefaultConfig)
}
if err = tlsconn.Handshake(); err != nil {
return nil, err
}
insecureSkipVerify := DefaultConfig.InsecureSkipVerify
if o.TLSConfig != nil {
insecureSkipVerify = o.TLSConfig.InsecureSkipVerify
}
if !insecureSkipVerify {
if err = tlsconn.VerifyHostname(host); err != nil {
return nil, err
}
}
client.conn = tlsconn
}
if err := client.init(&o); err != nil {
client.Close()
return nil, err
}
return client, nil
}
// NewClient creates a new connection to a host given as "hostname" or "hostname:port".
// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
// Default the port to 5222.
func NewClient(host, user, passwd string, debug bool) (*Client, error) {
opts := Options{
Host: host,
User: user,
Password: passwd,
Debug: debug,
Session: false,
}
return opts.NewClient()
}
// NewClientNoTLS creates a new client without TLS
func NewClientNoTLS(host, user, passwd string, debug bool) (*Client, error) {
opts := Options{
Host: host,
User: user,
Password: passwd,
NoTLS: true,
Debug: debug,
Session: false,
}
return opts.NewClient()
}
// Close closes the XMPP connection
func (c *Client) Close() error {
if c.conn != (*tls.Conn)(nil) {
return c.conn.Close()
}
return nil
}
func saslDigestResponse(username, realm, passwd, nonce, cnonceStr, authenticate, digestURI, nonceCountStr string) string {
h := func(text string) []byte {
h := md5.New()
h.Write([]byte(text))
return h.Sum(nil)
}
hex := func(bytes []byte) string {
return fmt.Sprintf("%x", bytes)
}
kd := func(secret, data string) []byte {
return h(secret + ":" + data)
}
a1 := string(h(username+":"+realm+":"+passwd)) + ":" + nonce + ":" + cnonceStr
a2 := authenticate + ":" + digestURI
response := hex(kd(hex(h(a1)), nonce+":"+nonceCountStr+":"+cnonceStr+":auth:"+hex(h(a2))))
return response
}
func cnonce() string {
randSize := big.NewInt(0)
randSize.Lsh(big.NewInt(1), 64)
cn, err := rand.Int(rand.Reader, randSize)
if err != nil {
return ""
}
return fmt.Sprintf("%016x", cn)
}
func (c *Client) init(o *Options) error {
var domain string
var user string
a := strings.SplitN(o.User, "@", 2)
if len(o.User) > 0 {
if len(a) != 2 {
return errors.New("xmpp: invalid username (want user@domain): " + o.User)
}
user = a[0]
domain = a[1]
} // Otherwise, we'll be attempting ANONYMOUS
// Declare intent to be a jabber client and gather stream features.
f, err := c.startStream(o, domain)
if err != nil {
return err
}
// If the server requires we STARTTLS, attempt to do so.
if f, err = c.startTLSIfRequired(f, o, domain); err != nil {
return err
}
if o.User == "" && o.Password == "" {
foundAnonymous := false
for _, m := range f.Mechanisms.Mechanism {
if m == "ANONYMOUS" {
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='ANONYMOUS' />\n", nsSASL)
foundAnonymous = true
break
}
}
if !foundAnonymous {
return fmt.Errorf("ANONYMOUS authentication is not an option and username and password were not specified")
}
} else {
// Even digest forms of authentication are unsafe if we do not know that the host
// we are talking to is the actual server, and not a man in the middle playing
// proxy.
if !c.IsEncrypted() && !o.InsecureAllowUnencryptedAuth {
return errors.New("refusing to authenticate over unencrypted TCP connection")
}
mechanism := ""
for _, m := range f.Mechanisms.Mechanism {
if m == "X-OAUTH2" && o.OAuthToken != "" && o.OAuthScope != "" {
mechanism = m
// Oauth authentication: send base64-encoded \x00 user \x00 token.
raw := "\x00" + user + "\x00" + o.OAuthToken
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
base64.StdEncoding.Encode(enc, []byte(raw))
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='X-OAUTH2' auth:service='oauth2' "+
"xmlns:auth='%s'>%s</auth>\n", nsSASL, o.OAuthXmlNs, enc)
break
}
if m == "PLAIN" {
mechanism = m
// Plain authentication: send base64-encoded \x00 user \x00 password.
raw := "\x00" + user + "\x00" + o.Password
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
base64.StdEncoding.Encode(enc, []byte(raw))
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>\n", nsSASL, enc)
break
}
if m == "DIGEST-MD5" {
mechanism = m
// Digest-MD5 authentication
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='DIGEST-MD5'/>\n", nsSASL)
var ch saslChallenge
if err = c.p.DecodeElement(&ch, nil); err != nil {
return errors.New("unmarshal <challenge>: " + err.Error())
}
b, err := base64.StdEncoding.DecodeString(string(ch))
if err != nil {
return err
}
tokens := map[string]string{}
for _, token := range strings.Split(string(b), ",") {
kv := strings.SplitN(strings.TrimSpace(token), "=", 2)
if len(kv) == 2 {
if kv[1][0] == '"' && kv[1][len(kv[1])-1] == '"' {
kv[1] = kv[1][1 : len(kv[1])-1]
}
tokens[kv[0]] = kv[1]
}
}
realm, _ := tokens["realm"]
nonce, _ := tokens["nonce"]
qop, _ := tokens["qop"]
charset, _ := tokens["charset"]
cnonceStr := cnonce()
digestURI := "xmpp/" + domain
nonceCount := fmt.Sprintf("%08x", 1)
digest := saslDigestResponse(user, realm, o.Password, nonce, cnonceStr, "AUTHENTICATE", digestURI, nonceCount)
message := "username=\"" + user + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", cnonce=\"" + cnonceStr +
"\", nc=" + nonceCount + ", qop=" + qop + ", digest-uri=\"" + digestURI + "\", response=" + digest + ", charset=" + charset
fmt.Fprintf(c.conn, "<response xmlns='%s'>%s</response>\n", nsSASL, base64.StdEncoding.EncodeToString([]byte(message)))
var rspauth saslRspAuth
if err = c.p.DecodeElement(&rspauth, nil); err != nil {
return errors.New("unmarshal <challenge>: " + err.Error())
}
b, err = base64.StdEncoding.DecodeString(string(rspauth))
if err != nil {
return err
}
fmt.Fprintf(c.conn, "<response xmlns='%s'/>\n", nsSASL)
break
}
}
if mechanism == "" {
return fmt.Errorf("PLAIN authentication is not an option: %v", f.Mechanisms.Mechanism)
}
}
// Next message should be either success or failure.
name, val, err := next(c.p)
if err != nil {
return err
}
switch v := val.(type) {
case *saslSuccess:
case *saslFailure:
// v.Any is type of sub-element in failure,
// which gives a description of what failed.
return errors.New("auth failure: " + v.Any.Local)
default:
return errors.New("expected <success> or <failure>, got <" + name.Local + "> in " + name.Space)
}
// Now that we're authenticated, we're supposed to start the stream over again.
// Declare intent to be a jabber client.
if f, err = c.startStream(o, domain); err != nil {
return err
}
// Generate a unique cookie
cookie := getCookie()
// Send IQ message asking to bind to the local user name.
if o.Resource == "" {
fmt.Fprintf(c.conn, "<iq type='set' id='%x'><bind xmlns='%s'></bind></iq>\n", cookie, nsBind)
} else {
fmt.Fprintf(c.conn, "<iq type='set' id='%x'><bind xmlns='%s'><resource>%s</resource></bind></iq>\n", cookie, nsBind, o.Resource)
}
var iq clientIQ
if err = c.p.DecodeElement(&iq, nil); err != nil {
return errors.New("unmarshal <iq>: " + err.Error())
}
if &iq.Bind == nil {
return errors.New("<iq> result missing <bind>")
}
c.jid = iq.Bind.Jid // our local id
c.domain = domain
if o.Session {
//if server support session, open it
fmt.Fprintf(c.conn, "<iq to='%s' type='set' id='%x'><session xmlns='%s'/></iq>", xmlEscape(domain), cookie, nsSession)
}
// We're connected and can now receive and send messages.
fmt.Fprintf(c.conn, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", o.Status, o.StatusMessage)
return nil
}
// startTlsIfRequired examines the server's stream features and, if STARTTLS is required or supported, performs the TLS handshake.
// f will be updated if the handshake completes, as the new stream's features are typically different from the original.
func (c *Client) startTLSIfRequired(f *streamFeatures, o *Options, domain string) (*streamFeatures, error) {
// whether we start tls is a matter of opinion: the server's and the user's.
switch {
case f.StartTLS == nil:
// the server does not support STARTTLS
return f, nil
case f.StartTLS.Required != nil:
// the server requires STARTTLS.
case !o.StartTLS:
// the user wants STARTTLS and the server supports it.
}
var err error
fmt.Fprintf(c.conn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>\n")
var k tlsProceed
if err = c.p.DecodeElement(&k, nil); err != nil {
return f, errors.New("unmarshal <proceed>: " + err.Error())
}
tc := o.TLSConfig
if tc == nil {
tc = new(tls.Config)
*tc = DefaultConfig
//TODO(scott): we should consider using the server's address or reverse lookup
tc.ServerName = domain
}
t := tls.Client(c.conn, tc)
if err = t.Handshake(); err != nil {
return f, errors.New("starttls handshake: " + err.Error())
}
c.conn = t
// restart our declaration of XMPP stream intentions.
tf, err := c.startStream(o, domain)
if err != nil {
return f, err
}
return tf, nil
}
// startStream will start a new XML decoder for the connection, signal the start of a stream to the server and verify that the server has
// also started the stream; if o.Debug is true, startStream will tee decoded XML data to stderr. The features advertised by the server
// will be returned.
func (c *Client) startStream(o *Options, domain string) (*streamFeatures, error) {
if o.Debug {
c.p = xml.NewDecoder(tee{c.conn, os.Stderr})
} else {
c.p = xml.NewDecoder(c.conn)
}
_, err := fmt.Fprintf(c.conn, "<?xml version='1.0'?>\n"+
"<stream:stream to='%s' xmlns='%s'\n"+
" xmlns:stream='%s' version='1.0'>\n",
xmlEscape(domain), nsClient, nsStream)
if err != nil {
return nil, err
}
// We expect the server to start a <stream>.
se, err := nextStart(c.p)
if err != nil {
return nil, err
}
if se.Name.Space != nsStream || se.Name.Local != "stream" {
return nil, fmt.Errorf("expected <stream> but got <%v> in %v", se.Name.Local, se.Name.Space)
}
// Now we're in the stream and can use Unmarshal.
// Next message should be <features> to tell us authentication options.
// See section 4.6 in RFC 3920.
f := new(streamFeatures)
if err = c.p.DecodeElement(f, nil); err != nil {
return f, errors.New("unmarshal <features>: " + err.Error())
}
return f, nil
}
// IsEncrypted will return true if the client is connected using a TLS transport, either because it used.
// TLS to connect from the outset, or because it successfully used STARTTLS to promote a TCP connection to TLS.
func (c *Client) IsEncrypted() bool {
_, ok := c.conn.(*tls.Conn)
return ok
}
// Chat is an incoming or outgoing XMPP chat message.
type Chat struct {
Remote string
Type string
Text string
Roster Roster
Other []string
Stamp time.Time
}
type Roster []Contact
type Contact struct {
Remote string
Name string
Group []string
}
// Presence is an XMPP presence notification.
type Presence struct {
From string
To string
Type string
Show string
Status string
}
type IQ struct {
ID string
From string
To string
Type string
Query []byte
}
// Recv waits to receive the next XMPP stanza.
// Return type is either a presence notification or a chat message.
func (c *Client) Recv() (stanza interface{}, err error) {
for {
_, val, err := next(c.p)
if err != nil {
return Chat{}, err
}
switch v := val.(type) {
case *clientMessage:
stamp, _ := time.Parse(
"2006-01-02T15:04:05Z",
v.Delay.Stamp,
)
chat := Chat{
Remote: v.From,
Type: v.Type,
Text: v.Body,
Other: v.Other,
Stamp: stamp,
}
return chat, nil
case *clientQuery:
var r Roster
for _, item := range v.Item {
r = append(r, Contact{item.Jid, item.Name, item.Group})
}
return Chat{Type: "roster", Roster: r}, nil
case *clientPresence:
return Presence{v.From, v.To, v.Type, v.Show, v.Status}, nil
case *clientIQ:
return IQ{ID: v.ID, From: v.From, To: v.To, Type: v.Type, Query: v.Query}, nil
}
}
}
// Send sends the message wrapped inside an XMPP message stanza body.
func (c *Client) Send(chat Chat) (n int, err error) {
return fmt.Fprintf(c.conn, "<message to='%s' type='%s' xml:lang='en'>"+"<body>%s</body></message>",
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text))
}
// SendOrg sends the original text without being wrapped in an XMPP message stanza.
func (c *Client) SendOrg(org string) (n int, err error) {
return fmt.Fprint(c.conn, org)
}
func (c *Client) SendPresence(presence Presence) (n int, err error) {
return fmt.Fprintf(c.conn, "<presence from='%s' to='%s'/>", xmlEscape(presence.From), xmlEscape(presence.To))
}
// SendHtml sends the message as HTML as defined by XEP-0071
func (c *Client) SendHtml(chat Chat) (n int, err error) {
return fmt.Fprintf(c.conn, "<message to='%s' type='%s' xml:lang='en'>"+
"<body>%s</body>"+
"<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'>%s</body></html></message>",
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text), chat.Text)
}
// Roster asks for the chat roster.
func (c *Client) Roster() error {
fmt.Fprintf(c.conn, "<iq from='%s' type='get' id='roster1'><query xmlns='jabber:iq:roster'/></iq>\n", xmlEscape(c.jid))
return nil
}
// RFC 3920 C.1 Streams name space
type streamFeatures struct {
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
StartTLS *tlsStartTLS
Mechanisms saslMechanisms
Bind bindBind
Session bool
}
type streamError struct {
XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
Any xml.Name
Text string
}
// RFC 3920 C.3 TLS name space
type tlsStartTLS struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
Required *string `xml:"required"`
}
type tlsProceed struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
}
type tlsFailure struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"`
}
// RFC 3920 C.4 SASL name space
type saslMechanisms struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
Mechanism []string `xml:"mechanism"`
}
type saslAuth struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
Mechanism string `xml:",attr"`
}
type saslChallenge string
type saslRspAuth string
type saslResponse string
type saslAbort struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl abort"`
}
type saslSuccess struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
}
type saslFailure struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
Any xml.Name `xml:",any"`
}
// RFC 3920 C.5 Resource binding name space
type bindBind struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
Resource string
Jid string `xml:"jid"`
}
// RFC 3921 B.1 jabber:client
type clientMessage struct {
XMLName xml.Name `xml:"jabber:client message"`
From string `xml:"from,attr"`
ID string `xml:"id,attr"`
To string `xml:"to,attr"`
Type string `xml:"type,attr"` // chat, error, groupchat, headline, or normal
// These should technically be []clientText, but string is much more convenient.
Subject string `xml:"subject"`
Body string `xml:"body"`
Thread string `xml:"thread"`
// Any hasn't matched element
Other []string `xml:",any"`
Delay Delay `xml:"delay"`
}
type Delay struct {
Stamp string `xml:"stamp,attr"`
}
type clientText struct {
Lang string `xml:",attr"`
Body string `xml:"chardata"`
}
type clientPresence struct {
XMLName xml.Name `xml:"jabber:client presence"`
From string `xml:"from,attr"`
ID string `xml:"id,attr"`
To string `xml:"to,attr"`
Type string `xml:"type,attr"` // error, probe, subscribe, subscribed, unavailable, unsubscribe, unsubscribed
Lang string `xml:"lang,attr"`
Show string `xml:"show"` // away, chat, dnd, xa
Status string `xml:"status"` // sb []clientText
Priority string `xml:"priority,attr"`
Error *clientError
}
type clientIQ struct { // info/query
XMLName xml.Name `xml:"jabber:client iq"`
From string `xml:"from,attr"`
ID string `xml:"id,attr"`
To string `xml:"to,attr"`
Type string `xml:"type,attr"` // error, get, result, set
Query []byte `xml:",innerxml"`
Error clientError
Bind bindBind
}
type clientError struct {
XMLName xml.Name `xml:"jabber:client error"`
Code string `xml:",attr"`
Type string `xml:",attr"`
Any xml.Name
Text string
}
type clientQuery struct {
Item []rosterItem
}
type rosterItem struct {
XMLName xml.Name `xml:"jabber:iq:roster item"`
Jid string `xml:",attr"`
Name string `xml:",attr"`
Subscription string `xml:",attr"`
Group []string
}
// Scan XML token stream to find next StartElement.
func nextStart(p *xml.Decoder) (xml.StartElement, error) {
for {
t, err := p.Token()
if err != nil && err != io.EOF || t == nil {
return xml.StartElement{}, err
}
switch t := t.(type) {
case xml.StartElement:
return t, nil
}
}
}
// Scan XML token stream for next element and save into val.
// If val == nil, allocate new element based on proto map.
// Either way, return val.
func next(p *xml.Decoder) (xml.Name, interface{}, error) {
// Read start element to find out what type we want.
se, err := nextStart(p)
if err != nil {
return xml.Name{}, nil, err
}
// Put it in an interface and allocate one.
var nv interface{}
switch se.Name.Space + " " + se.Name.Local {
case nsStream + " features":
nv = &streamFeatures{}
case nsStream + " error":
nv = &streamError{}
case nsTLS + " starttls":
nv = &tlsStartTLS{}
case nsTLS + " proceed":
nv = &tlsProceed{}
case nsTLS + " failure":
nv = &tlsFailure{}
case nsSASL + " mechanisms":
nv = &saslMechanisms{}
case nsSASL + " challenge":
nv = ""
case nsSASL + " response":
nv = ""
case nsSASL + " abort":
nv = &saslAbort{}
case nsSASL + " success":
nv = &saslSuccess{}
case nsSASL + " failure":
nv = &saslFailure{}
case nsBind + " bind":
nv = &bindBind{}
case nsClient + " message":
nv = &clientMessage{}
case nsClient + " presence":
nv = &clientPresence{}
case nsClient + " iq":
nv = &clientIQ{}
case nsClient + " error":
nv = &clientError{}
default:
return xml.Name{}, nil, errors.New("unexpected XMPP message " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
// Unmarshal into that storage.
if err = p.DecodeElement(nv, &se); err != nil {
return xml.Name{}, nil, err
}
return se.Name, nv, err
}
var xmlSpecial = map[byte]string{
'<': "&lt;",
'>': "&gt;",
'"': "&quot;",
'\'': "&apos;",
'&': "&amp;",
}
func xmlEscape(s string) string {
var b bytes.Buffer
for i := 0; i < len(s); i++ {
c := s[i]
if s, ok := xmlSpecial[c]; ok {
b.WriteString(s)
} else {
b.WriteByte(c)
}
}
return b.String()
}
type tee struct {
r io.Reader
w io.Writer
}
func (t tee) Read(p []byte) (n int, err error) {
n, err = t.r.Read(p)
if n > 0 {
t.w.Write(p[0:n])
t.w.Write([]byte("\n"))
}
return
}

View File

@ -0,0 +1,24 @@
package xmpp
import (
"fmt"
"strconv"
)
const IQTypeGet = "get"
const IQTypeSet = "set"
const IQTypeResult = "result"
func (c *Client) Discovery() (string, error) {
const namespace = "http://jabber.org/protocol/disco#items"
// use getCookie for a pseudo random id.
reqID := strconv.FormatUint(uint64(getCookie()), 10)
return c.RawInformationQuery(c.jid, c.domain, reqID, IQTypeGet, namespace, "")
}
// RawInformationQuery sends an information query request to the server.
func (c *Client) RawInformationQuery(from, to, id, iqType, requestNamespace, body string) (string, error) {
const xmlIQ = "<iq from='%s' to='%s' id='%s' type='%s'><query xmlns='%s'>%s</query></iq>"
_, err := fmt.Fprintf(c.conn, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, requestNamespace, body)
return id, err
}

134
vendor/github.com/mattn/go-xmpp/xmpp_muc.go generated vendored Normal file
View File

@ -0,0 +1,134 @@
// Copyright 2013 Flo Lauber <dev@qatfy.at>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// TODO(flo):
// - support password protected MUC rooms
// - cleanup signatures of join/leave functions
package xmpp
import (
"fmt"
"time"
"errors"
)
const (
nsMUC = "http://jabber.org/protocol/muc"
nsMUCUser = "http://jabber.org/protocol/muc#user"
NoHistory = 0
CharHistory = 1
StanzaHistory = 2
SecondsHistory = 3
SinceHistory = 4
)
// Send sends room topic wrapped inside an XMPP message stanza body.
func (c *Client) SendTopic(chat Chat) (n int, err error) {
return fmt.Fprintf(c.conn, "<message to='%s' type='%s' xml:lang='en'>"+"<subject>%s</subject></message>",
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text))
}
func (c *Client) JoinMUCNoHistory(jid, nick string) (n int, err error) {
if nick == "" {
nick = c.jid
}
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n"+
"<x xmlns='%s'>"+
"<history maxchars='0'/></x>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC)
}
// xep-0045 7.2
func (c *Client) JoinMUC(jid, nick string, history_type, history int, history_date *time.Time) (n int, err error) {
if nick == "" {
nick = c.jid
}
switch history_type {
case NoHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s' />\n" +
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC)
case CharHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<history maxchars='%d'/></x>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
case StanzaHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<history maxstanzas='%d'/></x>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
case SecondsHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<history seconds='%d'/></x>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
case SinceHistory:
if history_date != nil {
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<history since='%s'/></x>\n" +
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, history_date.Format(time.RFC3339))
}
}
return 0, errors.New("Unknown history option")
}
// xep-0045 7.2.6
func (c *Client) JoinProtectedMUC(jid, nick string, password string, history_type, history int, history_date *time.Time) (n int, err error) {
if nick == "" {
nick = c.jid
}
switch history_type {
case NoHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<password>%s</password>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password))
case CharHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<password>%s</password>\n"+
"<history maxchars='%d'/></x>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
case StanzaHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<password>%s</password>\n"+
"<history maxstanzas='%d'/></x>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
case SecondsHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<password>%s</password>\n"+
"<history seconds='%d'/></x>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
case SinceHistory:
if history_date != nil {
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<password>%s</password>\n"+
"<history since='%s'/></x>\n" +
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history_date.Format(time.RFC3339))
}
}
return 0, errors.New("Unknown history option")
}
// xep-0045 7.14
func (c *Client) LeaveMUC(jid string) (n int, err error) {
return fmt.Fprintf(c.conn, "<presence from='%s' to='%s' type='unavailable' />",
c.jid, xmlEscape(jid))
}

27
vendor/github.com/mattn/go-xmpp/xmpp_ping.go generated vendored Normal file
View File

@ -0,0 +1,27 @@
package xmpp
import (
"fmt"
)
func (c *Client) PingC2S(jid, server string) error {
if jid == "" {
jid = c.jid
}
if server == "" {
server = c.domain
}
_, err := fmt.Fprintf(c.conn, "<iq from='%s' to='%s' id='c2s1' type='get'>\n"+
"<ping xmlns='urn:xmpp:ping'/>\n"+
"</iq>",
xmlEscape(jid), xmlEscape(server))
return err
}
func (c *Client) PingS2S(fromServer, toServer string) error {
_, err := fmt.Fprintf(c.conn, "<iq from='%s' to='%s' id='s2s1' type='get'>\n"+
"<ping xmlns='urn:xmpp:ping'/>\n"+
"</iq>",
xmlEscape(fromServer), xmlEscape(toServer))
return err
}

20
vendor/github.com/mattn/go-xmpp/xmpp_subscription.go generated vendored Normal file
View File

@ -0,0 +1,20 @@
package xmpp
import (
"fmt"
)
func (c *Client) ApproveSubscription(jid string) {
fmt.Fprintf(c.conn, "<presence to='%s' type='subscribed'/>",
xmlEscape(jid))
}
func (c *Client) RevokeSubscription(jid string) {
fmt.Fprintf(c.conn, "<presence to='%s' type='unsubscribed'/>",
xmlEscape(jid))
}
func (c *Client) RequestSubscription(jid string) {
fmt.Fprintf(c.conn, "<presence to='%s' type='subscribe'/>",
xmlEscape(jid))
}

21
vendor/github.com/mreiferson/go-httpclient/LICENSE generated vendored Normal file
View File

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

View File

@ -0,0 +1,237 @@
/*
Provides an HTTP Transport that implements the `RoundTripper` interface and
can be used as a built in replacement for the standard library's, providing:
* connection timeouts
* request timeouts
This is a thin wrapper around `http.Transport` that sets dial timeouts and uses
Go's internal timer scheduler to call the Go 1.1+ `CancelRequest()` API.
*/
package httpclient
import (
"crypto/tls"
"errors"
"io"
"net"
"net/http"
"net/url"
"sync"
"time"
)
// returns the current version of the package
func Version() string {
return "0.4.1"
}
// Transport implements the RoundTripper interface and can be used as a replacement
// for Go's built in http.Transport implementing end-to-end request timeouts.
//
// transport := &httpclient.Transport{
// ConnectTimeout: 1*time.Second,
// ResponseHeaderTimeout: 5*time.Second,
// RequestTimeout: 10*time.Second,
// }
// defer transport.Close()
//
// client := &http.Client{Transport: transport}
// req, _ := http.NewRequest("GET", "http://127.0.0.1/test", nil)
// resp, err := client.Do(req)
// if err != nil {
// return err
// }
// defer resp.Body.Close()
//
type Transport struct {
// Proxy specifies a function to return a proxy for a given
// *http.Request. If the function returns a non-nil error, the
// request is aborted with the provided error.
// If Proxy is nil or returns a nil *url.URL, no proxy is used.
Proxy func(*http.Request) (*url.URL, error)
// Dial specifies the dial function for creating TCP
// connections. This will override the Transport's ConnectTimeout and
// ReadWriteTimeout settings.
// If Dial is nil, a dialer is generated on demand matching the Transport's
// options.
Dial func(network, addr string) (net.Conn, error)
// TLSClientConfig specifies the TLS configuration to use with
// tls.Client. If nil, the default configuration is used.
TLSClientConfig *tls.Config
// DisableKeepAlives, if true, prevents re-use of TCP connections
// between different HTTP requests.
DisableKeepAlives bool
// DisableCompression, if true, prevents the Transport from
// requesting compression with an "Accept-Encoding: gzip"
// request header when the Request contains no existing
// Accept-Encoding value. If the Transport requests gzip on
// its own and gets a gzipped response, it's transparently
// decoded in the Response.Body. However, if the user
// explicitly requested gzip it is not automatically
// uncompressed.
DisableCompression bool
// MaxIdleConnsPerHost, if non-zero, controls the maximum idle
// (keep-alive) to keep per-host. If zero,
// http.DefaultMaxIdleConnsPerHost is used.
MaxIdleConnsPerHost int
// ConnectTimeout, if non-zero, is the maximum amount of time a dial will wait for
// a connect to complete.
ConnectTimeout time.Duration
// ResponseHeaderTimeout, if non-zero, specifies the amount of
// time to wait for a server's response headers after fully
// writing the request (including its body, if any). This
// time does not include the time to read the response body.
ResponseHeaderTimeout time.Duration
// RequestTimeout, if non-zero, specifies the amount of time for the entire
// request to complete (including all of the above timeouts + entire response body).
// This should never be less than the sum total of the above two timeouts.
RequestTimeout time.Duration
// ReadWriteTimeout, if non-zero, will set a deadline for every Read and
// Write operation on the request connection.
ReadWriteTimeout time.Duration
// TCPWriteBufferSize, the size of the operating system's write
// buffer associated with the connection.
TCPWriteBufferSize int
// TCPReadBuffserSize, the size of the operating system's read
// buffer associated with the connection.
TCPReadBufferSize int
starter sync.Once
transport *http.Transport
}
// Close cleans up the Transport, currently a no-op
func (t *Transport) Close() error {
return nil
}
func (t *Transport) lazyStart() {
if t.Dial == nil {
t.Dial = func(netw, addr string) (net.Conn, error) {
c, err := net.DialTimeout(netw, addr, t.ConnectTimeout)
if err != nil {
return nil, err
}
if t.TCPReadBufferSize != 0 || t.TCPWriteBufferSize != 0 {
if tcpCon, ok := c.(*net.TCPConn); ok {
if t.TCPWriteBufferSize != 0 {
if err = tcpCon.SetWriteBuffer(t.TCPWriteBufferSize); err != nil {
return nil, err
}
}
if t.TCPReadBufferSize != 0 {
if err = tcpCon.SetReadBuffer(t.TCPReadBufferSize); err != nil {
return nil, err
}
}
} else {
err = errors.New("Not Tcp Connection")
return nil, err
}
}
if t.ReadWriteTimeout > 0 {
timeoutConn := &rwTimeoutConn{
TCPConn: c.(*net.TCPConn),
rwTimeout: t.ReadWriteTimeout,
}
return timeoutConn, nil
}
return c, nil
}
}
t.transport = &http.Transport{
Dial: t.Dial,
Proxy: t.Proxy,
TLSClientConfig: t.TLSClientConfig,
DisableKeepAlives: t.DisableKeepAlives,
DisableCompression: t.DisableCompression,
MaxIdleConnsPerHost: t.MaxIdleConnsPerHost,
ResponseHeaderTimeout: t.ResponseHeaderTimeout,
}
}
func (t *Transport) CancelRequest(req *http.Request) {
t.starter.Do(t.lazyStart)
t.transport.CancelRequest(req)
}
func (t *Transport) CloseIdleConnections() {
t.starter.Do(t.lazyStart)
t.transport.CloseIdleConnections()
}
func (t *Transport) RegisterProtocol(scheme string, rt http.RoundTripper) {
t.starter.Do(t.lazyStart)
t.transport.RegisterProtocol(scheme, rt)
}
func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
t.starter.Do(t.lazyStart)
if t.RequestTimeout > 0 {
timer := time.AfterFunc(t.RequestTimeout, func() {
t.transport.CancelRequest(req)
})
resp, err = t.transport.RoundTrip(req)
if err != nil {
timer.Stop()
} else {
resp.Body = &bodyCloseInterceptor{ReadCloser: resp.Body, timer: timer}
}
} else {
resp, err = t.transport.RoundTrip(req)
}
return
}
type bodyCloseInterceptor struct {
io.ReadCloser
timer *time.Timer
}
func (bci *bodyCloseInterceptor) Close() error {
bci.timer.Stop()
return bci.ReadCloser.Close()
}
// A net.Conn that sets a deadline for every Read or Write operation
type rwTimeoutConn struct {
*net.TCPConn
rwTimeout time.Duration
}
func (c *rwTimeoutConn) Read(b []byte) (int, error) {
err := c.TCPConn.SetDeadline(time.Now().Add(c.rwTimeout))
if err != nil {
return 0, err
}
return c.TCPConn.Read(b)
}
func (c *rwTimeoutConn) Write(b []byte) (int, error) {
err := c.TCPConn.SetDeadline(time.Now().Add(c.rwTimeout))
if err != nil {
return 0, err
}
return c.TCPConn.Write(b)
}

17
vendor/github.com/mrexodia/wray/examples/client.go generated vendored Normal file
View File

@ -0,0 +1,17 @@
package main
import "github.com/pythonandchips/wray"
import "fmt"
func main() {
wray.RegisterTransports([]wray.Transport{&wray.HttpTransport{}})
client := wray.NewFayeClient("http://localhost:5000/faye")
fmt.Println("subscribing")
client.Subscribe("/foo", false, func(message wray.Message) {
fmt.Println("-------------------------------------------")
fmt.Println(message.Data)
})
client.Listen()
}

15
vendor/github.com/mrexodia/wray/examples/publish.go generated vendored Normal file
View File

@ -0,0 +1,15 @@
package main
import "github.com/pythonandchips/wray"
import "fmt"
func main() {
wray.RegisterTransports([]wray.Transport{ &gofaye.HttpTransport{} })
client := wray.NewFayeClient("http://localhost:5000/faye")
params := map[string]interface{}{"hello": "from golang"}
fmt.Println("sending")
client.Publish("/foo", params)
}

140
vendor/github.com/mrexodia/wray/go_faye.go generated vendored Normal file
View File

@ -0,0 +1,140 @@
package wray
import (
"fmt"
"path/filepath"
"strings"
"time"
)
const (
UNCONNECTED = 1
CONNECTING = 2
CONNECTED = 3
DISCONNECTED = 4
HANDSHAKE = "handshake"
RETRY = "retry"
NONE = "none"
CONNECTION_TIMEOUT = 60.0
DEFAULT_RETRY = 5.0
MAX_REQUEST_SIZE = 2048
)
var (
MANDATORY_CONNECTION_TYPES = []string{"long-polling"}
registeredTransports = []Transport{}
)
type FayeClient struct {
state int
url string
subscriptions []Subscription
transport Transport
clientId string
schedular Schedular
}
type Subscription struct {
channel string
callback func(Message)
}
type SubscriptionPromise struct {
subscription Subscription
}
func NewFayeClient(url string) *FayeClient {
schedular := ChannelSchedular{}
client := &FayeClient{url: url, state: UNCONNECTED, schedular: schedular}
return client
}
func (self *FayeClient) handshake() {
t, err := SelectTransport(self, MANDATORY_CONNECTION_TYPES, []string{})
if err != nil {
panic("No usable transports available")
}
self.transport = t
self.transport.setUrl(self.url)
self.state = CONNECTING
handshakeParams := map[string]interface{}{"channel": "/meta/handshake",
"version": "1.0",
"supportedConnectionTypes": []string{"long-polling"}}
response, err := self.transport.send(handshakeParams)
if err != nil {
fmt.Println("Handshake failed. Retry in 10 seconds")
self.state = UNCONNECTED
self.schedular.wait(10*time.Second, func() {
fmt.Println("retying handshake")
self.handshake()
})
return
}
self.clientId = response.clientId
self.state = CONNECTED
self.transport, err = SelectTransport(self, response.supportedConnectionTypes, []string{})
if err != nil {
panic("Server does not support any available transports. Supported transports: " + strings.Join(response.supportedConnectionTypes, ","))
}
}
func (self *FayeClient) Subscribe(channel string, force bool, callback func(Message)) SubscriptionPromise {
if self.state == UNCONNECTED {
self.handshake()
}
subscriptionParams := map[string]interface{}{"channel": "/meta/subscribe", "clientId": self.clientId, "subscription": channel, "id": "1"}
subscription := Subscription{channel: channel, callback: callback}
//TODO: deal with subscription failures
self.transport.send(subscriptionParams)
self.subscriptions = append(self.subscriptions, subscription)
return SubscriptionPromise{subscription}
}
func (self *FayeClient) handleResponse(response Response) {
for _, message := range response.messages {
for _, subscription := range self.subscriptions {
matched, _ := filepath.Match(subscription.channel, message.Channel)
if matched {
go subscription.callback(message)
}
}
}
}
func (self *FayeClient) connect() {
connectParams := map[string]interface{}{"channel": "/meta/connect", "clientId": self.clientId, "connectionType": self.transport.connectionType()}
responseChannel := make(chan Response)
go func() {
response, _ := self.transport.send(connectParams)
responseChannel <- response
}()
self.listen(responseChannel)
}
func (self *FayeClient) listen(responseChannel chan Response) {
response := <-responseChannel
if response.successful == true {
go self.handleResponse(response)
} else {
}
}
func (self *FayeClient) Listen() {
for {
self.connect()
}
}
func (self *FayeClient) Publish(channel string, data map[string]interface{}) {
if self.state != CONNECTED {
self.handshake()
}
publishParams := map[string]interface{}{"channel": channel, "data": data, "clientId": self.clientId}
self.transport.send(publishParams)
}
func RegisterTransports(transports []Transport) {
registeredTransports = transports
}

55
vendor/github.com/mrexodia/wray/http_transport.go generated vendored Normal file
View File

@ -0,0 +1,55 @@
package wray
import (
"bytes"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/url"
)
type HttpTransport struct {
url string
SendHook func(data map[string]interface{})
}
func (self HttpTransport) isUsable(clientUrl string) bool {
parsedUrl, err := url.Parse(clientUrl)
if err != nil {
return false
}
if parsedUrl.Scheme == "http" || parsedUrl.Scheme == "https" {
return true
}
return false
}
func (self HttpTransport) connectionType() string {
return "long-polling"
}
func (self HttpTransport) send(data map[string]interface{}) (Response, error) {
if self.SendHook != nil {
self.SendHook(data)
}
dataBytes, _ := json.Marshal(data)
buffer := bytes.NewBuffer(dataBytes)
responseData, err := http.Post(self.url, "application/json", buffer)
if err != nil {
return Response{}, err
}
if responseData.StatusCode != 200 {
return Response{}, errors.New(responseData.Status)
}
readData, _ := ioutil.ReadAll(responseData.Body)
responseData.Body.Close()
var jsonData []interface{}
json.Unmarshal(readData, &jsonData)
response := newResponse(jsonData)
return response, nil
}
func (self *HttpTransport) setUrl(url string) {
self.url = url
}

61
vendor/github.com/mrexodia/wray/response.go generated vendored Normal file
View File

@ -0,0 +1,61 @@
package wray
type Response struct {
id string
channel string
successful bool
clientId string
supportedConnectionTypes []string
messages []Message
error error
}
type Message struct {
Channel string
Id string
Data map[string]interface{}
}
func newResponse(data []interface{}) Response {
headerData := data[0].(map[string]interface{})
messagesData := data[1.:]
messages := parseMessages(messagesData)
var id string
if headerData["id"] != nil {
id = headerData["id"].(string)
}
supportedConnectionTypes := []string{}
if headerData["supportedConnectionTypes"] != nil {
d := headerData["supportedConnectionTypes"].([]interface{})
for _, sct := range(d) {
supportedConnectionTypes = append(supportedConnectionTypes, sct.(string))
}
}
var clientId string
if headerData["clientId"] != nil {
clientId = headerData["clientId"].(string)
}
return Response{id: id,
clientId: clientId,
channel: headerData["channel"].(string),
successful: headerData["successful"].(bool),
messages: messages,
supportedConnectionTypes: supportedConnectionTypes}
}
func parseMessages(data []interface{}) []Message {
messages := []Message{}
for _, messageData := range(data) {
m := messageData.(map[string]interface{})
var id string
if m["id"] != nil {
id = m["id"].(string)
}
message := Message{Channel: m["channel"].(string),
Id: id,
Data: m["data"].(map[string]interface{})}
messages = append(messages, message)
}
return messages
}

22
vendor/github.com/mrexodia/wray/schedular.go generated vendored Normal file
View File

@ -0,0 +1,22 @@
package wray
import "time"
type Schedular interface {
wait(time.Duration, func())
delay() time.Duration
}
type ChannelSchedular struct {
}
func (self ChannelSchedular) wait(delay time.Duration, callback func()) {
go func() {
time.Sleep(delay)
callback()
}()
}
func (self ChannelSchedular) delay() time.Duration {
return (1 * time.Minute)
}

21
vendor/github.com/mrexodia/wray/transport.go generated vendored Normal file
View File

@ -0,0 +1,21 @@
package wray
import (
"errors"
)
type Transport interface {
isUsable(string) bool
connectionType() string
send(map[string]interface{}) (Response, error)
setUrl(string)
}
func SelectTransport(client *FayeClient, transportTypes []string, disabled []string) (Transport, error) {
for _, transport := range registeredTransports {
if contains(transport.connectionType(), transportTypes) && transport.isUsable(client.url) {
return transport, nil
}
}
return nil, errors.New("No usable transports available")
}

10
vendor/github.com/mrexodia/wray/utils.go generated vendored Normal file
View File

@ -0,0 +1,10 @@
package wray
func contains(target string, slice []string) bool {
for _, t := range(slice) {
if t == target {
return true
}
}
return false
}

23
vendor/github.com/nlopes/slack/LICENSE generated vendored Normal file
View File

@ -0,0 +1,23 @@
Copyright (c) 2015, Norberto Lopes
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

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

@ -0,0 +1,190 @@
package slack
import (
"errors"
"fmt"
"net/url"
)
type adminResponse struct {
OK bool `json:"ok"`
Error string `json:"error"`
}
func adminRequest(method string, teamName string, values url.Values, debug bool) (*adminResponse, error) {
adminResponse := &adminResponse{}
err := parseAdminResponse(method, teamName, values, adminResponse, debug)
if err != nil {
return nil, err
}
if !adminResponse.OK {
return nil, errors.New(adminResponse.Error)
}
return adminResponse, nil
}
// DisableUser disabled a user account, given a user ID
func (api *Client) DisableUser(teamName string, uid string) error {
values := url.Values{
"user": {uid},
"token": {api.config.token},
"set_active": {"true"},
"_attempts": {"1"},
}
_, err := adminRequest("setInactive", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err)
}
return nil
}
// InviteGuest invites a user to Slack as a single-channel guest
func (api *Client) InviteGuest(
teamName string,
channel string,
firstName string,
lastName string,
emailAddress string,
) error {
values := url.Values{
"email": {emailAddress},
"channels": {channel},
"first_name": {firstName},
"last_name": {lastName},
"ultra_restricted": {"1"},
"token": {api.config.token},
"set_active": {"true"},
"_attempts": {"1"},
}
_, err := adminRequest("invite", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to invite single-channel guest: %s", err)
}
return nil
}
// InviteRestricted invites a user to Slack as a restricted account
func (api *Client) InviteRestricted(
teamName string,
channel string,
firstName string,
lastName string,
emailAddress string,
) error {
values := url.Values{
"email": {emailAddress},
"channels": {channel},
"first_name": {firstName},
"last_name": {lastName},
"restricted": {"1"},
"token": {api.config.token},
"set_active": {"true"},
"_attempts": {"1"},
}
_, err := adminRequest("invite", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to restricted account: %s", err)
}
return nil
}
// InviteToTeam invites a user to a Slack team
func (api *Client) InviteToTeam(
teamName string,
firstName string,
lastName string,
emailAddress string,
) error {
values := url.Values{
"email": {emailAddress},
"first_name": {firstName},
"last_name": {lastName},
"token": {api.config.token},
"set_active": {"true"},
"_attempts": {"1"},
}
_, err := adminRequest("invite", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to invite to team: %s", err)
}
return nil
}
// SetRegular enables the specified user
func (api *Client) SetRegular(teamName string, user string) error {
values := url.Values{
"user": {user},
"token": {api.config.token},
"set_active": {"true"},
"_attempts": {"1"},
}
_, err := adminRequest("setRegular", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err)
}
return nil
}
// SendSSOBindingEmail sends an SSO binding email to the specified user
func (api *Client) SendSSOBindingEmail(teamName string, user string) error {
values := url.Values{
"user": {user},
"token": {api.config.token},
"set_active": {"true"},
"_attempts": {"1"},
}
_, err := adminRequest("sendSSOBind", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err)
}
return nil
}
// SetUltraRestricted converts a user into a single-channel guest
func (api *Client) SetUltraRestricted(teamName, uid, channel string) error {
values := url.Values{
"user": {uid},
"channel": {channel},
"token": {api.config.token},
"set_active": {"true"},
"_attempts": {"1"},
}
_, err := adminRequest("setUltraRestricted", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to ultra-restrict account: %s", err)
}
return nil
}
// SetRestricted converts a user into a restricted account
func (api *Client) SetRestricted(teamName, uid string) error {
values := url.Values{
"user": {uid},
"token": {api.config.token},
"set_active": {"true"},
"_attempts": {"1"},
}
_, err := adminRequest("setRestricted", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to restrict account: %s", err)
}
return nil
}

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

@ -0,0 +1,76 @@
package slack
// AttachmentField contains information for an attachment field
// An Attachment can contain multiple of these
type AttachmentField struct {
Title string `json:"title"`
Value string `json:"value"`
Short bool `json:"short"`
}
// AttachmentAction is a button to be included in the attachment. Required when
// using message buttons and otherwise not useful. A maximum of 5 actions may be
// provided per attachment.
type AttachmentAction struct {
Name string `json:"name"` // Required.
Text string `json:"text"` // Required.
Style string `json:"style,omitempty"` // Optional. Allowed values: "default", "primary", "danger"
Type string `json:"type"` // Required. Must be set to "button"
Value string `json:"value,omitempty"` // Optional.
Confirm []ConfirmationField `json:"confirm,omitempty"` // Optional.
}
// AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction)
type AttachmentActionCallback struct {
Actions []AttachmentAction `json:"actions"`
CallbackID string `json:"callback_id"`
Team Team `json:"team"`
Channel Channel `json:"channel"`
User User `json:"user"`
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"`
}
// ConfirmationField are used to ask users to confirm actions
type ConfirmationField struct {
Title string `json:"title,omitempty"` // Optional.
Text string `json:"text"` // Required.
OkText string `json:"ok_text,omitempty"` // Optional. Defaults to "Okay"
DismissText string `json:"dismiss_text,omitempty"` // Optional. Defaults to "Cancel"
}
// Attachment contains all the information for an attachment
type Attachment struct {
Color string `json:"color,omitempty"`
Fallback string `json:"fallback"`
CallbackID string `json:"callback_id,omitempty"`
AuthorName string `json:"author_name,omitempty"`
AuthorSubname string `json:"author_subname,omitempty"`
AuthorLink string `json:"author_link,omitempty"`
AuthorIcon string `json:"author_icon,omitempty"`
Title string `json:"title,omitempty"`
TitleLink string `json:"title_link,omitempty"`
Pretext string `json:"pretext,omitempty"`
Text string `json:"text"`
ImageURL string `json:"image_url,omitempty"`
ThumbURL string `json:"thumb_url,omitempty"`
Fields []AttachmentField `json:"fields,omitempty"`
Actions []AttachmentAction `json:"actions,omitempty"`
MarkdownIn []string `json:"mrkdwn_in,omitempty"`
Footer string `json:"footer,omitempty"`
FooterIcon string `json:"footer_icon,omitempty"`
Ts int64 `json:"ts,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 == true {
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
}

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

@ -0,0 +1,261 @@
package slack
import (
"errors"
"net/url"
"strconv"
)
type channelResponseFull struct {
Channel Channel `json:"channel"`
Channels []Channel `json:"channels"`
Purpose string `json:"purpose"`
Topic string `json:"topic"`
NotInChannel bool `json:"not_in_channel"`
History
SlackResponse
}
// Channel contains information about the channel
type Channel struct {
groupConversation
IsChannel bool `json:"is_channel"`
IsGeneral bool `json:"is_general"`
IsMember bool `json:"is_member"`
}
func channelRequest(path string, values url.Values, debug bool) (*channelResponseFull, error) {
response := &channelResponseFull{}
err := post(path, values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// ArchiveChannel archives the given channel
func (api *Client) ArchiveChannel(channel string) error {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
}
_, err := channelRequest("channels.archive", values, api.debug)
if err != nil {
return err
}
return nil
}
// UnarchiveChannel unarchives the given channel
func (api *Client) UnarchiveChannel(channel string) error {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
}
_, err := channelRequest("channels.unarchive", values, api.debug)
if err != nil {
return err
}
return nil
}
// CreateChannel creates a channel with the given name and returns a *Channel
func (api *Client) CreateChannel(channel string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"name": {channel},
}
response, err := channelRequest("channels.create", values, api.debug)
if err != nil {
return nil, err
}
return &response.Channel, nil
}
// GetChannelHistory retrieves the channel history
func (api *Client) GetChannelHistory(channel string, params HistoryParameters) (*History, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
}
if params.Latest != DEFAULT_HISTORY_LATEST {
values.Add("latest", params.Latest)
}
if params.Oldest != DEFAULT_HISTORY_OLDEST {
values.Add("oldest", params.Oldest)
}
if params.Count != DEFAULT_HISTORY_COUNT {
values.Add("count", strconv.Itoa(params.Count))
}
if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE {
if params.Inclusive {
values.Add("inclusive", "1")
} else {
values.Add("inclusive", "0")
}
}
if params.Unreads != DEFAULT_HISTORY_UNREADS {
if params.Unreads {
values.Add("unreads", "1")
} else {
values.Add("unreads", "0")
}
}
response, err := channelRequest("channels.history", values, api.debug)
if err != nil {
return nil, err
}
return &response.History, nil
}
// GetChannelInfo retrieves the given channel
func (api *Client) GetChannelInfo(channel string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
}
response, err := channelRequest("channels.info", values, api.debug)
if err != nil {
return nil, err
}
return &response.Channel, nil
}
// InviteUserToChannel invites a user to a given channel and returns a *Channel
func (api *Client) InviteUserToChannel(channel, user string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"user": {user},
}
response, err := channelRequest("channels.invite", values, api.debug)
if err != nil {
return nil, err
}
return &response.Channel, nil
}
// JoinChannel joins the currently authenticated user to a channel
func (api *Client) JoinChannel(channel string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"name": {channel},
}
response, err := channelRequest("channels.join", values, api.debug)
if err != nil {
return nil, err
}
return &response.Channel, nil
}
// LeaveChannel makes the authenticated user leave the given channel
func (api *Client) LeaveChannel(channel string) (bool, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
}
response, err := channelRequest("channels.leave", values, api.debug)
if err != nil {
return false, err
}
if response.NotInChannel {
return response.NotInChannel, nil
}
return false, nil
}
// KickUserFromChannel kicks a user from a given channel
func (api *Client) KickUserFromChannel(channel, user string) error {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"user": {user},
}
_, err := channelRequest("channels.kick", values, api.debug)
if err != nil {
return err
}
return nil
}
// GetChannels retrieves all the channels
func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) {
values := url.Values{
"token": {api.config.token},
}
if excludeArchived {
values.Add("exclude_archived", "1")
}
response, err := channelRequest("channels.list", values, api.debug)
if err != nil {
return nil, err
}
return response.Channels, nil
}
// SetChannelReadMark sets the read mark of a given channel to a specific point
// Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a
// timer before making the call. In this way, any further updates needed during the timeout will not generate extra calls
// (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A
// timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout.
func (api *Client) SetChannelReadMark(channel, ts string) error {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"ts": {ts},
}
_, err := channelRequest("channels.mark", values, api.debug)
if err != nil {
return err
}
return nil
}
// RenameChannel renames a given channel
func (api *Client) RenameChannel(channel, name string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"name": {name},
}
// XXX: the created entry in this call returns a string instead of a number
// so I may have to do some workaround to solve it.
response, err := channelRequest("channels.rename", values, api.debug)
if err != nil {
return nil, err
}
return &response.Channel, nil
}
// SetChannelPurpose sets the channel purpose and returns the purpose that was
// successfully set
func (api *Client) SetChannelPurpose(channel, purpose string) (string, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"purpose": {purpose},
}
response, err := channelRequest("channels.setPurpose", values, api.debug)
if err != nil {
return "", err
}
return response.Purpose, nil
}
// SetChannelTopic sets the channel topic and returns the topic that was successfully set
func (api *Client) SetChannelTopic(channel, topic string) (string, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"topic": {topic},
}
response, err := channelRequest("channels.setTopic", values, api.debug)
if err != nil {
return "", err
}
return response.Topic, nil
}

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

@ -0,0 +1,166 @@
package slack
import (
"encoding/json"
"errors"
"net/url"
"strings"
)
const (
DEFAULT_MESSAGE_USERNAME = ""
DEFAULT_MESSAGE_ASUSER = false
DEFAULT_MESSAGE_PARSE = ""
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"`
Text string `json:"text"`
SlackResponse
}
// PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request
type PostMessageParameters struct {
Text string
Username string
AsUser bool
Parse string
LinkNames int
Attachments []Attachment
UnfurlLinks bool
UnfurlMedia bool
IconURL string
IconEmoji string
Markdown bool `json:"mrkdwn,omitempty"`
EscapeText bool
}
// NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set
func NewPostMessageParameters() PostMessageParameters {
return PostMessageParameters{
Username: DEFAULT_MESSAGE_USERNAME,
AsUser: DEFAULT_MESSAGE_ASUSER,
Parse: DEFAULT_MESSAGE_PARSE,
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,
}
}
func chatRequest(path string, values url.Values, debug bool) (*chatResponseFull, error) {
response := &chatResponseFull{}
err := post(path, values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// DeleteMessage deletes a message in a channel
func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"ts": {messageTimestamp},
}
response, err := chatRequest("chat.delete", values, api.debug)
if err != nil {
return "", "", err
}
return response.Channel, response.Timestamp, nil
}
func escapeMessage(message string) string {
replacer := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
return replacer.Replace(message)
}
// PostMessage sends a message to a channel.
// Message is escaped by default according to https://api.slack.com/docs/formatting
// Use http://davestevens.github.io/slack-message-builder/ to help crafting your message.
func (api *Client) PostMessage(channel, text string, params PostMessageParameters) (string, string, error) {
if params.EscapeText {
text = escapeMessage(text)
}
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"text": {text},
}
if params.Username != DEFAULT_MESSAGE_USERNAME {
values.Set("username", string(params.Username))
}
if params.AsUser != DEFAULT_MESSAGE_ASUSER {
values.Set("as_user", "true")
}
if params.Parse != DEFAULT_MESSAGE_PARSE {
values.Set("parse", string(params.Parse))
}
if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES {
values.Set("link_names", "1")
}
if params.Attachments != nil {
attachments, err := json.Marshal(params.Attachments)
if err != nil {
return "", "", err
}
values.Set("attachments", string(attachments))
}
if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS {
values.Set("unfurl_links", "true")
}
// I want to send a message with explicit `as_user` `true` and `unfurl_links` `false` in request.
// Because setting `as_user` to `true` will change the default value for `unfurl_links` to `true` on Slack API side.
if params.AsUser != DEFAULT_MESSAGE_ASUSER && params.UnfurlLinks == DEFAULT_MESSAGE_UNFURL_LINKS {
values.Set("unfurl_links", "false")
}
if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA {
values.Set("unfurl_media", "false")
}
if params.IconURL != DEFAULT_MESSAGE_ICON_URL {
values.Set("icon_url", params.IconURL)
}
if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI {
values.Set("icon_emoji", params.IconEmoji)
}
if params.Markdown != DEFAULT_MESSAGE_MARKDOWN {
values.Set("mrkdwn", "false")
}
response, err := chatRequest("chat.postMessage", values, api.debug)
if err != nil {
return "", "", err
}
return response.Channel, response.Timestamp, nil
}
// UpdateMessage updates a message in a channel
func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"text": {escapeMessage(text)},
"ts": {timestamp},
}
response, err := chatRequest("chat.update", values, api.debug)
if err != nil {
return "", "", "", err
}
return response.Channel, response.Timestamp, response.Text, nil
}

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

@ -0,0 +1,10 @@
package slack
// Comment contains all the information relative to a comment
type Comment struct {
ID string `json:"id,omitempty"`
Created JSONTime `json:"created,omitempty"`
Timestamp JSONTime `json:"timestamp,omitempty"`
User string `json:"user,omitempty"`
Comment string `json:"comment,omitempty"`
}

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

@ -0,0 +1,38 @@
package slack
// Conversation is the foundation for IM and BaseGroupConversation
type conversation struct {
ID string `json:"id"`
Created JSONTime `json:"created"`
IsOpen bool `json:"is_open"`
LastRead string `json:"last_read,omitempty"`
Latest *Message `json:"latest,omitempty"`
UnreadCount int `json:"unread_count,omitempty"`
UnreadCountDisplay int `json:"unread_count_display,omitempty"`
}
// GroupConversation is the foundation for Group and Channel
type groupConversation struct {
conversation
Name string `json:"name"`
Creator string `json:"creator"`
IsArchived bool `json:"is_archived"`
Members []string `json:"members"`
NumMembers int `json:"num_members,omitempty"`
Topic Topic `json:"topic"`
Purpose Purpose `json:"purpose"`
}
// Topic contains information about the topic
type Topic struct {
Value string `json:"value"`
Creator string `json:"creator"`
LastSet JSONTime `json:"last_set"`
}
// Purpose contains information about the purpose
type Purpose struct {
Value string `json:"value"`
Creator string `json:"creator"`
LastSet JSONTime `json:"last_set"`
}

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

@ -0,0 +1,123 @@
package slack
import (
"errors"
"net/url"
"strconv"
"strings"
)
type SnoozeDebug struct {
SnoozeEndDate string `json:"snooze_end_date"`
}
type SnoozeInfo struct {
SnoozeEnabled bool `json:"snooze_enabled,omitempty"`
SnoozeEndTime int `json:"snooze_endtime,omitempty"`
SnoozeRemaining int `json:"snooze_remaining,omitempty"`
SnoozeDebug SnoozeDebug `json:"snooze_debug,omitempty"`
}
type DNDStatus struct {
Enabled bool `json:"dnd_enabled"`
NextStartTimestamp int `json:"next_dnd_start_ts"`
NextEndTimestamp int `json:"next_dnd_end_ts"`
SnoozeInfo
}
type dndResponseFull struct {
DNDStatus
SlackResponse
}
type dndTeamInfoResponse struct {
Users map[string]DNDStatus `json:"users"`
SlackResponse
}
func dndRequest(path string, values url.Values, debug bool) (*dndResponseFull, error) {
response := &dndResponseFull{}
err := post(path, values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// EndDND ends the user's scheduled Do Not Disturb session
func (api *Client) EndDND() error {
values := url.Values{
"token": {api.config.token},
}
response := &SlackResponse{}
if err := post("dnd.endDnd", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
}
// EndSnooze ends the current user's snooze mode
func (api *Client) EndSnooze() (*DNDStatus, error) {
values := url.Values{
"token": {api.config.token},
}
response, err := dndRequest("dnd.endSnooze", values, api.debug)
if err != nil {
return nil, err
}
return &response.DNDStatus, nil
}
// GetDNDInfo provides information about a user's current Do Not Disturb settings.
func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) {
values := url.Values{
"token": {api.config.token},
}
if user != nil {
values.Set("user", *user)
}
response, err := dndRequest("dnd.info", values, api.debug)
if err != nil {
return nil, err
}
return &response.DNDStatus, nil
}
// GetDNDTeamInfo provides information about a user's current Do Not Disturb settings.
func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error) {
values := url.Values{
"token": {api.config.token},
"users": {strings.Join(users, ",")},
}
response := &dndTeamInfoResponse{}
if err := post("dnd.teamInfo", values, response, api.debug); err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response.Users, nil
}
// SetSnooze adjusts the snooze duration for a user's Do Not Disturb
// settings. If a snooze session is not already active for the user, invoking
// this method will begin one for the specified duration.
func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) {
values := url.Values{
"token": {api.config.token},
"num_minutes": {strconv.Itoa(minutes)},
}
response, err := dndRequest("dnd.setSnooze", values, api.debug)
if err != nil {
return nil, err
}
return &response.DNDStatus, nil
}

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

@ -0,0 +1,27 @@
package slack
import (
"errors"
"net/url"
)
type emojiResponseFull struct {
Emoji map[string]string `json:"emoji"`
SlackResponse
}
// GetEmoji retrieves all the emojis
func (api *Client) GetEmoji() (map[string]string, error) {
values := url.Values{
"token": {api.config.token},
}
response := &emojiResponseFull{}
err := post("emoji.list", values, response, api.debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response.Emoji, nil
}

View File

@ -0,0 +1,19 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
channels, err := api.GetChannels(false)
if err != nil {
fmt.Printf("%s\n", err)
return
}
for _, channel := range channels {
fmt.Println(channel.ID)
}
}

30
vendor/github.com/nlopes/slack/examples/files/files.go generated vendored Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
params := slack.FileUploadParameters{
Title: "Batman Example",
//Filetype: "txt",
File: "example.txt",
//Content: "Nan Nan Nan Nan Nan Nan Nan Nan Batman",
}
file, err := api.UploadFile(params)
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("Name: %s, URL: %s\n", file.Name, file.URL)
err = api.DeleteFile(file.ID)
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("File %s deleted successfully.\n", file.Name)
}

View File

@ -0,0 +1,22 @@
package main
import (
"fmt"
"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
// api.SetDebug(true)
groups, err := api.GetGroups(false)
if err != nil {
fmt.Printf("%s\n", err)
return
}
for _, group := range groups {
fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name)
}
}

View File

@ -0,0 +1,32 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
params := slack.PostMessageParameters{}
attachment := slack.Attachment{
Pretext: "some pretext",
Text: "some text",
// Uncomment the following part to send a field too
/*
Fields: []slack.AttachmentField{
slack.AttachmentField{
Title: "a",
Value: "no",
},
},
*/
}
params.Attachments = []slack.Attachment{attachment}
channelID, timestamp, err := api.PostMessage("CHANNEL_ID", "Some text", params)
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("Message successfully sent to channel %s at %s", channelID, timestamp)
}

123
vendor/github.com/nlopes/slack/examples/pins/pins.go generated vendored Normal file
View File

@ -0,0 +1,123 @@
package main
import (
"flag"
"fmt"
"github.com/nlopes/slack"
)
/*
WARNING: This example is destructive in the sense that it create a channel called testpinning
*/
func main() {
var (
apiToken string
debug bool
)
flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token")
flag.BoolVar(&debug, "debug", false, "Show JSON output")
flag.Parse()
api := slack.New(apiToken)
if debug {
api.SetDebug(true)
}
var (
postAsUserName string
postAsUserID string
postToChannelID string
)
// Find the user to post as.
authTest, err := api.AuthTest()
if err != nil {
fmt.Printf("Error getting channels: %s\n", err)
return
}
channelName := "testpinning"
// Post as the authenticated user.
postAsUserName = authTest.User
postAsUserID = authTest.UserID
// Create a temporary channel
channel, err := api.CreateChannel(channelName)
if err != nil {
// If the channel exists, that means we just need to unarchive it
if err.Error() == "name_taken" {
err = nil
channels, err := api.GetChannels(false)
if err != nil {
fmt.Println("Could not retrieve channels")
return
}
for _, archivedChannel := range channels {
if archivedChannel.Name == channelName {
if archivedChannel.IsArchived {
err = api.UnarchiveChannel(archivedChannel.ID)
if err != nil {
fmt.Printf("Could not unarchive %s: %s\n", archivedChannel.ID, err)
return
}
}
channel = &archivedChannel
break
}
}
}
if err != nil {
fmt.Printf("Error setting test channel for pinning: %s\n", err)
return
}
}
postToChannelID = channel.ID
fmt.Printf("Posting as %s (%s) in channel %s\n", postAsUserName, postAsUserID, postToChannelID)
// Post a message.
postParams := slack.PostMessageParameters{}
channelID, timestamp, err := api.PostMessage(postToChannelID, "Is this any good?", postParams)
if err != nil {
fmt.Printf("Error posting message: %s\n", err)
return
}
// Grab a reference to the message.
msgRef := slack.NewRefToMessage(channelID, timestamp)
// Add message pin to channel
if err := api.AddPin(channelID, msgRef); err != nil {
fmt.Printf("Error adding pin: %s\n", err)
return
}
// List all of the users pins.
listPins, _, err := api.ListPins(channelID)
if err != nil {
fmt.Printf("Error listing pins: %s\n", err)
return
}
fmt.Printf("\n")
fmt.Printf("All pins by %s...\n", authTest.User)
for _, item := range listPins {
fmt.Printf(" > Item type: %s\n", item.Type)
}
// Remove the pin.
err = api.RemovePin(channelID, msgRef)
if err != nil {
fmt.Printf("Error remove pin: %s\n", err)
return
}
if err = api.ArchiveChannel(channelID); err != nil {
fmt.Printf("Error archiving channel: %s\n", err)
return
}
}

View File

@ -0,0 +1,126 @@
package main
import (
"flag"
"fmt"
"github.com/nlopes/slack"
)
func main() {
var (
apiToken string
debug bool
)
flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token")
flag.BoolVar(&debug, "debug", false, "Show JSON output")
flag.Parse()
api := slack.New(apiToken)
if debug {
api.SetDebug(true)
}
var (
postAsUserName string
postAsUserID string
postToUserName string
postToUserID string
postToChannelID string
)
// Find the user to post as.
authTest, err := api.AuthTest()
if err != nil {
fmt.Printf("Error getting channels: %s\n", err)
return
}
// Post as the authenticated user.
postAsUserName = authTest.User
postAsUserID = authTest.UserID
// Posting to DM with self causes a conversation with slackbot.
postToUserName = authTest.User
postToUserID = authTest.UserID
// Find the channel.
_, _, chanID, err := api.OpenIMChannel(postToUserID)
if err != nil {
fmt.Printf("Error opening IM: %s\n", err)
return
}
postToChannelID = chanID
fmt.Printf("Posting as %s (%s) in DM with %s (%s), channel %s\n", postAsUserName, postAsUserID, postToUserName, postToUserID, postToChannelID)
// Post a message.
postParams := slack.PostMessageParameters{}
channelID, timestamp, err := api.PostMessage(postToChannelID, "Is this any good?", postParams)
if err != nil {
fmt.Printf("Error posting message: %s\n", err)
return
}
// Grab a reference to the message.
msgRef := slack.NewRefToMessage(channelID, timestamp)
// React with :+1:
if err := api.AddReaction("+1", msgRef); err != nil {
fmt.Printf("Error adding reaction: %s\n", err)
return
}
// React with :-1:
if err := api.AddReaction("cry", msgRef); err != nil {
fmt.Printf("Error adding reaction: %s\n", err)
return
}
// Get all reactions on the message.
msgReactions, err := api.GetReactions(msgRef, slack.NewGetReactionsParameters())
if err != nil {
fmt.Printf("Error getting reactions: %s\n", err)
return
}
fmt.Printf("\n")
fmt.Printf("%d reactions to message...\n", len(msgReactions))
for _, r := range msgReactions {
fmt.Printf(" %d users say %s\n", r.Count, r.Name)
}
// List all of the users reactions.
listReactions, _, err := api.ListReactions(slack.NewListReactionsParameters())
if err != nil {
fmt.Printf("Error listing reactions: %s\n", err)
return
}
fmt.Printf("\n")
fmt.Printf("All reactions by %s...\n", authTest.User)
for _, item := range listReactions {
fmt.Printf("%d on a %s...\n", len(item.Reactions), item.Type)
for _, r := range item.Reactions {
fmt.Printf(" %s (along with %d others)\n", r.Name, r.Count-1)
}
}
// Remove the :cry: reaction.
err = api.RemoveReaction("cry", msgRef)
if err != nil {
fmt.Printf("Error remove reaction: %s\n", err)
return
}
// Get all reactions on the message.
msgReactions, err = api.GetReactions(msgRef, slack.NewGetReactionsParameters())
if err != nil {
fmt.Printf("Error getting reactions: %s\n", err)
return
}
fmt.Printf("\n")
fmt.Printf("%d reactions to message after removing cry...\n", len(msgReactions))
for _, r := range msgReactions {
fmt.Printf(" %d users say %s\n", r.Count, r.Name)
}
}

46
vendor/github.com/nlopes/slack/examples/stars/stars.go generated vendored Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"flag"
"fmt"
"github.com/nlopes/slack"
)
func main() {
var (
apiToken string
debug bool
)
flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token")
flag.BoolVar(&debug, "debug", false, "Show JSON output")
flag.Parse()
api := slack.New(apiToken)
if debug {
api.SetDebug(true)
}
// Get all stars for the usr.
params := slack.NewStarsParameters()
starredItems, _, err := api.GetStarred(params)
if err != nil {
fmt.Printf("Error getting stars: %s\n", err)
return
}
for _, s := range starredItems {
var desc string
switch s.Type {
case slack.TYPE_MESSAGE:
desc = s.Message.Text
case slack.TYPE_FILE:
desc = s.File.Name
case slack.TYPE_FILE_COMMENT:
desc = s.File.Name + " - " + s.Comment.Comment
case slack.TYPE_CHANNEL, slack.TYPE_IM, slack.TYPE_GROUP:
desc = s.Channel
}
fmt.Printf("Starred %s: %s\n", s.Type, desc)
}
}

17
vendor/github.com/nlopes/slack/examples/users/users.go generated vendored Normal file
View File

@ -0,0 +1,17 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
user, err := api.GetUserInfo("U023BECGF")
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email)
}

View File

@ -0,0 +1,58 @@
package main
import (
"fmt"
"log"
"os"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR TOKEN HERE")
logger := log.New(os.Stdout, "slack-bot: ", log.Lshortfile|log.LstdFlags)
slack.SetLogger(logger)
api.SetDebug(true)
rtm := api.NewRTM()
go rtm.ManageConnection()
Loop:
for {
select {
case msg := <-rtm.IncomingEvents:
fmt.Print("Event Received: ")
switch ev := msg.Data.(type) {
case *slack.HelloEvent:
// Ignore hello
case *slack.ConnectedEvent:
fmt.Println("Infos:", ev.Info)
fmt.Println("Connection counter:", ev.ConnectionCount)
// Replace #general with your Channel ID
rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", "#general"))
case *slack.MessageEvent:
fmt.Printf("Message: %v\n", ev)
case *slack.PresenceChangeEvent:
fmt.Printf("Presence Change: %v\n", ev)
case *slack.LatencyReport:
fmt.Printf("Current latency: %v\n", ev.Value)
case *slack.RTMError:
fmt.Printf("Error: %s\n", ev.Error())
case *slack.InvalidAuthEvent:
fmt.Printf("Invalid credentials")
break Loop
default:
// Ignore other events..
// fmt.Printf("Unexpected: %v\n", msg.Data)
}
}
}
}

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

@ -0,0 +1,274 @@
package slack
import (
"errors"
"net/url"
"strconv"
"strings"
)
const (
// Add here the defaults in the siten
DEFAULT_FILES_USER = ""
DEFAULT_FILES_CHANNEL = ""
DEFAULT_FILES_TS_FROM = 0
DEFAULT_FILES_TS_TO = -1
DEFAULT_FILES_TYPES = "all"
DEFAULT_FILES_COUNT = 100
DEFAULT_FILES_PAGE = 1
)
// File contains all the information for a file
type File struct {
ID string `json:"id"`
Created JSONTime `json:"created"`
Timestamp JSONTime `json:"timestamp"`
Name string `json:"name"`
Title string `json:"title"`
Mimetype string `json:"mimetype"`
ImageExifRotation int `json:"image_exif_rotation"`
Filetype string `json:"filetype"`
PrettyType string `json:"pretty_type"`
User string `json:"user"`
Mode string `json:"mode"`
Editable bool `json:"editable"`
IsExternal bool `json:"is_external"`
ExternalType string `json:"external_type"`
Size int `json:"size"`
URL string `json:"url"` // Deprecated - never set
URLDownload string `json:"url_download"` // Deprecated - never set
URLPrivate string `json:"url_private"`
URLPrivateDownload string `json:"url_private_download"`
OriginalH int `json:"original_h"`
OriginalW int `json:"original_w"`
Thumb64 string `json:"thumb_64"`
Thumb80 string `json:"thumb_80"`
Thumb160 string `json:"thumb_160"`
Thumb360 string `json:"thumb_360"`
Thumb360Gif string `json:"thumb_360_gif"`
Thumb360W int `json:"thumb_360_w"`
Thumb360H int `json:"thumb_360_h"`
Thumb480 string `json:"thumb_480"`
Thumb480W int `json:"thumb_480_w"`
Thumb480H int `json:"thumb_480_h"`
Thumb720 string `json:"thumb_720"`
Thumb720W int `json:"thumb_720_w"`
Thumb720H int `json:"thumb_720_h"`
Thumb960 string `json:"thumb_960"`
Thumb960W int `json:"thumb_960_w"`
Thumb960H int `json:"thumb_960_h"`
Thumb1024 string `json:"thumb_1024"`
Thumb1024W int `json:"thumb_1024_w"`
Thumb1024H int `json:"thumb_1024_h"`
Permalink string `json:"permalink"`
PermalinkPublic string `json:"permalink_public"`
EditLink string `json:"edit_link"`
Preview string `json:"preview"`
PreviewHighlight string `json:"preview_highlight"`
Lines int `json:"lines"`
LinesMore int `json:"lines_more"`
IsPublic bool `json:"is_public"`
PublicURLShared bool `json:"public_url_shared"`
Channels []string `json:"channels"`
Groups []string `json:"groups"`
IMs []string `json:"ims"`
InitialComment Comment `json:"initial_comment"`
CommentsCount int `json:"comments_count"`
NumStars int `json:"num_stars"`
IsStarred bool `json:"is_starred"`
}
// FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request
type FileUploadParameters struct {
File string
Content string
Filetype string
Filename string
Title string
InitialComment string
Channels []string
}
// GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request
type GetFilesParameters struct {
User string
Channel string
TimestampFrom JSONTime
TimestampTo JSONTime
Types string
Count int
Page int
}
type fileResponseFull struct {
File `json:"file"`
Paging `json:"paging"`
Comments []Comment `json:"comments"`
Files []File `json:"files"`
SlackResponse
}
// NewGetFilesParameters provides an instance of GetFilesParameters with all the sane default values set
func NewGetFilesParameters() GetFilesParameters {
return GetFilesParameters{
User: DEFAULT_FILES_USER,
Channel: DEFAULT_FILES_CHANNEL,
TimestampFrom: DEFAULT_FILES_TS_FROM,
TimestampTo: DEFAULT_FILES_TS_TO,
Types: DEFAULT_FILES_TYPES,
Count: DEFAULT_FILES_COUNT,
Page: DEFAULT_FILES_PAGE,
}
}
func fileRequest(path string, values url.Values, debug bool) (*fileResponseFull, error) {
response := &fileResponseFull{}
err := post(path, values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// GetFileInfo retrieves a file and related comments
func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment, *Paging, error) {
values := url.Values{
"token": {api.config.token},
"file": {fileID},
"count": {strconv.Itoa(count)},
"page": {strconv.Itoa(page)},
}
response, err := fileRequest("files.info", values, api.debug)
if err != nil {
return nil, nil, nil, err
}
return &response.File, response.Comments, &response.Paging, nil
}
// GetFiles retrieves all files according to the parameters given
func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) {
values := url.Values{
"token": {api.config.token},
}
if params.User != DEFAULT_FILES_USER {
values.Add("user", params.User)
}
if params.Channel != DEFAULT_FILES_CHANNEL {
values.Add("channel", params.Channel)
}
// XXX: this is broken. fix it with a proper unix timestamp
if params.TimestampFrom != DEFAULT_FILES_TS_FROM {
values.Add("ts_from", params.TimestampFrom.String())
}
if params.TimestampTo != DEFAULT_FILES_TS_TO {
values.Add("ts_to", params.TimestampTo.String())
}
if params.Types != DEFAULT_FILES_TYPES {
values.Add("types", params.Types)
}
if params.Count != DEFAULT_FILES_COUNT {
values.Add("count", strconv.Itoa(params.Count))
}
if params.Page != DEFAULT_FILES_PAGE {
values.Add("page", strconv.Itoa(params.Page))
}
response, err := fileRequest("files.list", values, api.debug)
if err != nil {
return nil, nil, err
}
return response.Files, &response.Paging, nil
}
// UploadFile uploads a file
func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) {
// Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More
// investigation needed, but for now this will do.
_, err = api.AuthTest()
if err != nil {
return nil, err
}
response := &fileResponseFull{}
values := url.Values{
"token": {api.config.token},
}
if params.Filetype != "" {
values.Add("filetype", params.Filetype)
}
if params.Filename != "" {
values.Add("filename", params.Filename)
}
if params.Title != "" {
values.Add("title", params.Title)
}
if params.InitialComment != "" {
values.Add("initial_comment", params.InitialComment)
}
if len(params.Channels) != 0 {
values.Add("channels", strings.Join(params.Channels, ","))
}
if params.Content != "" {
values.Add("content", params.Content)
err = post("files.upload", values, response, api.debug)
} else if params.File != "" {
err = postWithMultipartResponse("files.upload", params.File, values, response, api.debug)
}
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return &response.File, nil
}
// DeleteFile deletes a file
func (api *Client) DeleteFile(fileID string) error {
values := url.Values{
"token": {api.config.token},
"file": {fileID},
}
_, err := fileRequest("files.delete", values, api.debug)
if err != nil {
return err
}
return nil
}
// RevokeFilePublicURL disables public/external sharing for a file
func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) {
values := url.Values{
"token": {api.config.token},
"file": {fileID},
}
response, err := fileRequest("files.revokePublicURL", values, api.debug)
if err != nil {
return nil, err
}
return &response.File, nil
}
// ShareFilePublicURL enabled public/external sharing for a file
func (api *Client) ShareFilePublicURL(fileID string) (*File, []Comment, *Paging, error) {
values := url.Values{
"token": {api.config.token},
"file": {fileID},
}
response, err := fileRequest("files.sharedPublicURL", values, api.debug)
if err != nil {
return nil, nil, nil, err
}
return &response.File, response.Comments, &response.Paging, nil
}

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

@ -0,0 +1,293 @@
package slack
import (
"errors"
"net/url"
"strconv"
)
// Group contains all the information for a group
type Group struct {
groupConversation
IsGroup bool `json:"is_group"`
}
type groupResponseFull struct {
Group Group `json:"group"`
Groups []Group `json:"groups"`
Purpose string `json:"purpose"`
Topic string `json:"topic"`
NotInGroup bool `json:"not_in_group"`
NoOp bool `json:"no_op"`
AlreadyClosed bool `json:"already_closed"`
AlreadyOpen bool `json:"already_open"`
AlreadyInGroup bool `json:"already_in_group"`
Channel Channel `json:"channel"`
History
SlackResponse
}
func groupRequest(path string, values url.Values, debug bool) (*groupResponseFull, error) {
response := &groupResponseFull{}
err := post(path, values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// ArchiveGroup archives a private group
func (api *Client) ArchiveGroup(group string) error {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
_, err := groupRequest("groups.archive", values, api.debug)
if err != nil {
return err
}
return nil
}
// UnarchiveGroup unarchives a private group
func (api *Client) UnarchiveGroup(group string) error {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
_, err := groupRequest("groups.unarchive", values, api.debug)
if err != nil {
return err
}
return nil
}
// CreateGroup creates a private group
func (api *Client) CreateGroup(group string) (*Group, error) {
values := url.Values{
"token": {api.config.token},
"name": {group},
}
response, err := groupRequest("groups.create", values, api.debug)
if err != nil {
return nil, err
}
return &response.Group, nil
}
// CreateChildGroup creates a new private group archiving the old one
// This method takes an existing private group and performs the following steps:
// 1. Renames the existing group (from "example" to "example-archived").
// 2. Archives the existing group.
// 3. Creates a new group with the name of the existing group.
// 4. Adds all members of the existing group to the new group.
func (api *Client) CreateChildGroup(group string) (*Group, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
response, err := groupRequest("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) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
response, err := imRequest("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) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
if params.Latest != DEFAULT_HISTORY_LATEST {
values.Add("latest", params.Latest)
}
if params.Oldest != DEFAULT_HISTORY_OLDEST {
values.Add("oldest", params.Oldest)
}
if params.Count != DEFAULT_HISTORY_COUNT {
values.Add("count", strconv.Itoa(params.Count))
}
if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE {
if params.Inclusive {
values.Add("inclusive", "1")
} else {
values.Add("inclusive", "0")
}
}
if params.Unreads != DEFAULT_HISTORY_UNREADS {
if params.Unreads {
values.Add("unreads", "1")
} else {
values.Add("unreads", "0")
}
}
response, err := groupRequest("groups.history", values, api.debug)
if err != nil {
return nil, err
}
return &response.History, nil
}
// InviteUserToGroup invites a specific user to a private group
func (api *Client) InviteUserToGroup(group, user string) (*Group, bool, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
"user": {user},
}
response, err := groupRequest("groups.invite", values, api.debug)
if err != nil {
return nil, false, err
}
return &response.Group, response.AlreadyInGroup, nil
}
// LeaveGroup makes authenticated user leave the group
func (api *Client) LeaveGroup(group string) error {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
_, err := groupRequest("groups.leave", values, api.debug)
if err != nil {
return err
}
return nil
}
// KickUserFromGroup kicks a user from a group
func (api *Client) KickUserFromGroup(group, user string) error {
values := url.Values{
"token": {api.config.token},
"channel": {group},
"user": {user},
}
_, err := groupRequest("groups.kick", values, api.debug)
if err != nil {
return err
}
return nil
}
// GetGroups retrieves all groups
func (api *Client) GetGroups(excludeArchived bool) ([]Group, error) {
values := url.Values{
"token": {api.config.token},
}
if excludeArchived {
values.Add("exclude_archived", "1")
}
response, err := groupRequest("groups.list", values, api.debug)
if err != nil {
return nil, err
}
return response.Groups, nil
}
// GetGroupInfo retrieves the given group
func (api *Client) GetGroupInfo(group string) (*Group, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
response, err := groupRequest("groups.info", values, api.debug)
if err != nil {
return nil, err
}
return &response.Group, nil
}
// SetGroupReadMark sets the read mark on a private group
// Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a
// timer before making the call. In this way, any further updates needed during the timeout will not generate extra
// calls (just one per channel). This is useful for when reading scroll-back history, or following a busy live
// channel. A timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout.
func (api *Client) SetGroupReadMark(group, ts string) error {
values := url.Values{
"token": {api.config.token},
"channel": {group},
"ts": {ts},
}
_, err := groupRequest("groups.mark", values, api.debug)
if err != nil {
return err
}
return nil
}
// OpenGroup opens a private group
func (api *Client) OpenGroup(group string) (bool, bool, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
}
response, err := groupRequest("groups.open", values, api.debug)
if err != nil {
return false, false, err
}
return response.NoOp, response.AlreadyOpen, nil
}
// RenameGroup renames a group
// XXX: They return a channel, not a group. What is this crap? :(
// Inconsistent api it seems.
func (api *Client) RenameGroup(group, name string) (*Channel, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
"name": {name},
}
// XXX: the created entry in this call returns a string instead of a number
// so I may have to do some workaround to solve it.
response, err := groupRequest("groups.rename", values, api.debug)
if err != nil {
return nil, err
}
return &response.Channel, nil
}
// SetGroupPurpose sets the group purpose
func (api *Client) SetGroupPurpose(group, purpose string) (string, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
"purpose": {purpose},
}
response, err := groupRequest("groups.setPurpose", values, api.debug)
if err != nil {
return "", err
}
return response.Purpose, nil
}
// SetGroupTopic sets the group topic
func (api *Client) SetGroupTopic(group, topic string) (string, error) {
values := url.Values{
"token": {api.config.token},
"channel": {group},
"topic": {topic},
}
response, err := groupRequest("groups.setTopic", values, api.debug)
if err != nil {
return "", err
}
return response.Topic, nil
}

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

@ -0,0 +1,36 @@
package slack
const (
DEFAULT_HISTORY_LATEST = ""
DEFAULT_HISTORY_OLDEST = "0"
DEFAULT_HISTORY_COUNT = 100
DEFAULT_HISTORY_INCLUSIVE = false
DEFAULT_HISTORY_UNREADS = false
)
// HistoryParameters contains all the necessary information to help in the retrieval of history for Channels/Groups/DMs
type HistoryParameters struct {
Latest string
Oldest string
Count int
Inclusive bool
Unreads bool
}
// History contains message history information needed to navigate a Channel / Group / DM history
type History struct {
Latest string `json:"latest"`
Messages []Message `json:"messages"`
HasMore bool `json:"has_more"`
}
// NewHistoryParameters provides an instance of HistoryParameters with all the sane default values set
func NewHistoryParameters() HistoryParameters {
return HistoryParameters{
Latest: DEFAULT_HISTORY_LATEST,
Oldest: DEFAULT_HISTORY_OLDEST,
Count: DEFAULT_HISTORY_COUNT,
Inclusive: DEFAULT_HISTORY_INCLUSIVE,
Unreads: DEFAULT_HISTORY_UNREADS,
}
}

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

@ -0,0 +1,130 @@
package slack
import (
"errors"
"net/url"
"strconv"
)
type imChannel struct {
ID string `json:"id"`
}
type imResponseFull struct {
NoOp bool `json:"no_op"`
AlreadyClosed bool `json:"already_closed"`
AlreadyOpen bool `json:"already_open"`
Channel imChannel `json:"channel"`
IMs []IM `json:"ims"`
History
SlackResponse
}
// IM contains information related to the Direct Message channel
type IM struct {
conversation
IsIM bool `json:"is_im"`
User string `json:"user"`
IsUserDeleted bool `json:"is_user_deleted"`
}
func imRequest(path string, values url.Values, debug bool) (*imResponseFull, error) {
response := &imResponseFull{}
err := post(path, values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// CloseIMChannel closes the direct message channel
func (api *Client) CloseIMChannel(channel string) (bool, bool, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
}
response, err := imRequest("im.close", values, api.debug)
if err != nil {
return false, false, err
}
return response.NoOp, response.AlreadyClosed, nil
}
// OpenIMChannel opens a direct message channel to the user provided as argument
// Returns some status and the channel ID
func (api *Client) OpenIMChannel(user string) (bool, bool, string, error) {
values := url.Values{
"token": {api.config.token},
"user": {user},
}
response, err := imRequest("im.open", values, api.debug)
if err != nil {
return false, false, "", err
}
return response.NoOp, response.AlreadyOpen, response.Channel.ID, nil
}
// MarkIMChannel sets the read mark of a direct message channel to a specific point
func (api *Client) MarkIMChannel(channel, ts string) (err error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
"ts": {ts},
}
_, err = imRequest("im.mark", values, api.debug)
if err != nil {
return err
}
return
}
// GetIMHistory retrieves the direct message channel history
func (api *Client) GetIMHistory(channel string, params HistoryParameters) (*History, error) {
values := url.Values{
"token": {api.config.token},
"channel": {channel},
}
if params.Latest != DEFAULT_HISTORY_LATEST {
values.Add("latest", params.Latest)
}
if params.Oldest != DEFAULT_HISTORY_OLDEST {
values.Add("oldest", params.Oldest)
}
if params.Count != DEFAULT_HISTORY_COUNT {
values.Add("count", strconv.Itoa(params.Count))
}
if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE {
if params.Inclusive {
values.Add("inclusive", "1")
} else {
values.Add("inclusive", "0")
}
}
if params.Unreads != DEFAULT_HISTORY_UNREADS {
if params.Unreads {
values.Add("unreads", "1")
} else {
values.Add("unreads", "0")
}
}
response, err := imRequest("im.history", values, api.debug)
if err != nil {
return nil, err
}
return &response.History, nil
}
// GetIMChannels returns the list of direct message channels
func (api *Client) GetIMChannels() ([]IM, error) {
values := url.Values{
"token": {api.config.token},
}
response, err := imRequest("im.list", values, api.debug)
if err != nil {
return nil, err
}
return response.IMs, nil
}

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

@ -0,0 +1,206 @@
package slack
import (
"fmt"
"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)
}
// 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 {
Image48 string `json:"image_48"`
}
// Bot contains information about a bot
type Bot struct {
ID string `json:"id"`
Name string `json:"name"`
Deleted bool `json:"deleted"`
Icons Icons `json:"icons"`
}
// 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
WebResponse
}
// 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
}

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

@ -0,0 +1,75 @@
package slack
const (
TYPE_MESSAGE = "message"
TYPE_FILE = "file"
TYPE_FILE_COMMENT = "file_comment"
TYPE_CHANNEL = "channel"
TYPE_IM = "im"
TYPE_GROUP = "group"
)
// Item is any type of slack message - message, file, or file comment.
type Item struct {
Type string `json:"type"`
Channel string `json:"channel,omitempty"`
Message *Message `json:"message,omitempty"`
File *File `json:"file,omitempty"`
Comment *Comment `json:"comment,omitempty"`
Timestamp string `json:"ts,omitempty"`
}
// NewMessageItem turns a message on a channel into a typed message struct.
func NewMessageItem(ch string, m *Message) Item {
return Item{Type: TYPE_MESSAGE, Channel: ch, Message: m}
}
// NewFileItem turns a file into a typed file struct.
func NewFileItem(f *File) Item {
return Item{Type: TYPE_FILE, File: f}
}
// NewFileCommentItem turns a file and comment into a typed file_comment struct.
func NewFileCommentItem(f *File, c *Comment) Item {
return Item{Type: TYPE_FILE_COMMENT, File: f, Comment: c}
}
// NewChannelItem turns a channel id into a typed channel struct.
func NewChannelItem(ch string) Item {
return Item{Type: TYPE_CHANNEL, Channel: ch}
}
// NewIMItem turns a channel id into a typed im struct.
func NewIMItem(ch string) Item {
return Item{Type: TYPE_IM, Channel: ch}
}
// NewGroupItem turns a channel id into a typed group struct.
func NewGroupItem(ch string) Item {
return Item{Type: TYPE_GROUP, Channel: ch}
}
// ItemRef is a reference to a message of any type. One of FileID,
// CommentId, or the combination of ChannelId and Timestamp must be
// specified.
type ItemRef struct {
Channel string `json:"channel"`
Timestamp string `json:"timestamp"`
File string `json:"file"`
Comment string `json:"file_comment"`
}
// NewRefToMessage initializes a reference to to a message.
func NewRefToMessage(channel, timestamp string) ItemRef {
return ItemRef{Channel: channel, Timestamp: timestamp}
}
// NewRefToFile initializes a reference to a file.
func NewRefToFile(file string) ItemRef {
return ItemRef{File: file}
}
// NewRefToComment initializes a reference to a file comment.
func NewRefToComment(comment string) ItemRef {
return ItemRef{Comment: comment}
}

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

@ -0,0 +1,30 @@
package slack
import "sync"
// IDGenerator provides an interface for generating integer ID values.
type IDGenerator interface {
Next() int
}
// NewSafeID returns a new instance of an IDGenerator which is safe for
// concurrent use by multiple goroutines.
func NewSafeID(startID int) IDGenerator {
return &safeID{
nextID: startID,
mutex: &sync.Mutex{},
}
}
type safeID struct {
nextID int
mutex *sync.Mutex
}
func (s *safeID) Next() int {
s.mutex.Lock()
defer s.mutex.Unlock()
id := s.nextID
s.nextID++
return id
}

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

@ -0,0 +1,131 @@
package slack
// OutgoingMessage is used for the realtime API, and seems incomplete.
type OutgoingMessage struct {
ID int `json:"id"`
Channel string `json:"channel,omitempty"`
Text string `json:"text,omitempty"`
Type string `json:"type,omitempty"`
}
// Message is an auxiliary type to allow us to have a message containing sub messages
type Message struct {
Msg
SubMessage *Msg `json:"message,omitempty"`
}
// Msg contains information about a slack message
type Msg struct {
// Basic Message
Type string `json:"type,omitempty"`
Channel string `json:"channel,omitempty"`
User string `json:"user,omitempty"`
Text string `json:"text,omitempty"`
Timestamp string `json:"ts,omitempty"`
IsStarred bool `json:"is_starred,omitempty"`
PinnedTo []string `json:"pinned_to, omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
Edited *Edited `json:"edited,omitempty"`
// Message Subtypes
SubType string `json:"subtype,omitempty"`
// Hidden Subtypes
Hidden bool `json:"hidden,omitempty"` // message_changed, message_deleted, unpinned_item
DeletedTimestamp string `json:"deleted_ts,omitempty"` // message_deleted
EventTimestamp string `json:"event_ts,omitempty"`
// bot_message (https://api.slack.com/events/message/bot_message)
BotID string `json:"bot_id,omitempty"`
Username string `json:"username,omitempty"`
Icons *Icon `json:"icons,omitempty"`
// channel_join, group_join
Inviter string `json:"inviter,omitempty"`
// channel_topic, group_topic
Topic string `json:"topic,omitempty"`
// channel_purpose, group_purpose
Purpose string `json:"purpose,omitempty"`
// channel_name, group_name
Name string `json:"name,omitempty"`
OldName string `json:"old_name,omitempty"`
// channel_archive, group_archive
Members []string `json:"members,omitempty"`
// file_share, file_comment, file_mention
File *File `json:"file,omitempty"`
// file_share
Upload bool `json:"upload,omitempty"`
// file_comment
Comment *Comment `json:"comment,omitempty"`
// pinned_item
ItemType string `json:"item_type,omitempty"`
// https://api.slack.com/rtm
ReplyTo int `json:"reply_to,omitempty"`
Team string `json:"team,omitempty"`
// reactions
Reactions []ItemReaction `json:"reactions,omitempty"`
}
// Icon is used for bot messages
type Icon struct {
IconURL string `json:"icon_url,omitempty"`
IconEmoji string `json:"icon_emoji,omitempty"`
}
// Edited indicates that a message has been edited.
type Edited struct {
User string `json:"user,omitempty"`
Timestamp string `json:"ts,omitempty"`
}
// Event contains the event type
type Event struct {
Type string `json:"type,omitempty"`
}
// Ping contains information about a Ping Event
type Ping struct {
ID int `json:"id"`
Type string `json:"type"`
}
// Pong contains information about a Pong Event
type Pong struct {
Type string `json:"type"`
ReplyTo int `json:"reply_to"`
}
// NewOutgoingMessage prepares an OutgoingMessage that the user can
// use to send a message. Use this function to properly set the
// messageID.
func (rtm *RTM) NewOutgoingMessage(text string, channel string) *OutgoingMessage {
id := rtm.idGen.Next()
return &OutgoingMessage{
ID: id,
Type: "message",
Channel: channel,
Text: text,
}
}
// NewTypingMessage prepares an OutgoingMessage that the user can
// use to send as a typing indicator. Use this function to properly set the
// messageID.
func (rtm *RTM) NewTypingMessage(channel string) *OutgoingMessage {
id := rtm.idGen.Next()
return &OutgoingMessage{
ID: id,
Type: "typing",
Channel: channel,
}
}

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

@ -0,0 +1,119 @@
package slack
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"time"
)
var HTTPClient = &http.Client{}
type WebResponse struct {
Ok bool `json:"ok"`
Error *WebError `json:"error"`
}
type WebError string
func (s WebError) Error() string {
return string(s)
}
func fileUploadReq(path, fpath string, values url.Values) (*http.Request, error) {
fullpath, err := filepath.Abs(fpath)
if err != nil {
return nil, err
}
file, err := os.Open(fullpath)
if err != nil {
return nil, err
}
defer file.Close()
body := &bytes.Buffer{}
wr := multipart.NewWriter(body)
ioWriter, err := wr.CreateFormFile("file", filepath.Base(fullpath))
if err != nil {
wr.Close()
return nil, err
}
bytes, err := io.Copy(ioWriter, file)
if err != nil {
wr.Close()
return nil, err
}
// Close the multipart writer or the footer won't be written
wr.Close()
stat, err := file.Stat()
if err != nil {
return nil, err
}
if bytes != stat.Size() {
return nil, errors.New("could not read the whole file")
}
req, err := http.NewRequest("POST", path, body)
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))
}
err = json.Unmarshal(response, &intf)
if err != nil {
return err
}
return nil
}
func postWithMultipartResponse(path string, filepath string, values url.Values, intf interface{}, debug bool) error {
req, err := fileUploadReq(SLACK_API+path, filepath, values)
resp, err := HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return parseResponseBody(resp.Body, &intf, debug)
}
func postForm(endpoint string, values url.Values, intf interface{}, debug bool) error {
resp, err := HTTPClient.PostForm(endpoint, values)
if err != nil {
return err
}
defer resp.Body.Close()
return parseResponseBody(resp.Body, &intf, debug)
}
func post(path string, values url.Values, intf interface{}, debug bool) error {
return postForm(SLACK_API+path, values, intf, debug)
}
func parseAdminResponse(method string, teamName string, values url.Values, intf interface{}, debug bool) error {
endpoint := fmt.Sprintf(SLACK_WEB_API_FORMAT, teamName, method, time.Now().Unix())
return postForm(endpoint, values, intf, debug)
}

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

@ -0,0 +1,54 @@
package slack
import (
"errors"
"net/url"
)
type OAuthResponseIncomingWebhook struct {
URL string `json:"url"`
Channel string `json:"channel"`
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"`
SlackResponse
}
// GetOAuthToken retrieves an AccessToken
func GetOAuthToken(clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) {
response, err := GetOAuthResponse(clientID, clientSecret, code, redirectURI, debug)
if err != nil {
return "", "", err
}
return response.AccessToken, response.Scope, nil
}
func GetOAuthResponse(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 = post("oauth.access", values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}

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

@ -0,0 +1,20 @@
package slack
// Paging contains paging information
type Paging struct {
Count int `json:"count"`
Total int `json:"total"`
Page int `json:"page"`
Pages int `json:"pages"`
}
// Pagination contains pagination information
// This is different from Paging in that it contains additional details
type Pagination struct {
TotalCount int `json:"total_count"`
Page int `json:"page"`
PerPage int `json:"per_page"`
PageCount int `json:"page_count"`
First int `json:"first"`
Last int `json:"last"`
}

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

@ -0,0 +1,79 @@
package slack
import (
"errors"
"net/url"
)
type listPinsResponseFull struct {
Items []Item
Paging `json:"paging"`
SlackResponse
}
// AddPin pins an item in a channel
func (api *Client) AddPin(channel string, item ItemRef) error {
values := url.Values{
"channel": {channel},
"token": {api.config.token},
}
if item.Timestamp != "" {
values.Set("timestamp", string(item.Timestamp))
}
if item.File != "" {
values.Set("file", string(item.File))
}
if item.Comment != "" {
values.Set("file_comment", string(item.Comment))
}
response := &SlackResponse{}
if err := post("pins.add", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
}
// RemovePin un-pins an item from a channel
func (api *Client) RemovePin(channel string, item ItemRef) error {
values := url.Values{
"channel": {channel},
"token": {api.config.token},
}
if item.Timestamp != "" {
values.Set("timestamp", string(item.Timestamp))
}
if item.File != "" {
values.Set("file", string(item.File))
}
if item.Comment != "" {
values.Set("file_comment", string(item.Comment))
}
response := &SlackResponse{}
if err := post("pins.remove", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
}
// ListPins returns information about the items a user reacted to.
func (api *Client) ListPins(channel string) ([]Item, *Paging, error) {
values := url.Values{
"channel": {channel},
"token": {api.config.token},
}
response := &listPinsResponseFull{}
err := post("pins.list", values, response, api.debug)
if err != nil {
return nil, nil, err
}
if !response.Ok {
return nil, nil, errors.New(response.Error)
}
return response.Items, &response.Paging, nil
}

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

@ -0,0 +1,246 @@
package slack
import (
"errors"
"net/url"
"strconv"
)
// ItemReaction is the reactions that have happened on an item.
type ItemReaction struct {
Name string `json:"name"`
Count int `json:"count"`
Users []string `json:"users"`
}
// ReactedItem is an item that was reacted to, and the details of the
// reactions.
type ReactedItem struct {
Item
Reactions []ItemReaction
}
// GetReactionsParameters is the inputs to get reactions to an item.
type GetReactionsParameters struct {
Full bool
}
// NewGetReactionsParameters initializes the inputs to get reactions to an item.
func NewGetReactionsParameters() GetReactionsParameters {
return GetReactionsParameters{
Full: false,
}
}
type getReactionsResponseFull struct {
Type string
M struct {
Reactions []ItemReaction
} `json:"message"`
F struct {
Reactions []ItemReaction
} `json:"file"`
FC struct {
Reactions []ItemReaction
} `json:"comment"`
SlackResponse
}
func (res getReactionsResponseFull) extractReactions() []ItemReaction {
switch res.Type {
case "message":
return res.M.Reactions
case "file":
return res.F.Reactions
case "file_comment":
return res.FC.Reactions
}
return []ItemReaction{}
}
const (
DEFAULT_REACTIONS_USER = ""
DEFAULT_REACTIONS_COUNT = 100
DEFAULT_REACTIONS_PAGE = 1
DEFAULT_REACTIONS_FULL = false
)
// ListReactionsParameters is the inputs to find all reactions by a user.
type ListReactionsParameters struct {
User string
Count int
Page int
Full bool
}
// NewListReactionsParameters initializes the inputs to find all reactions
// performed by a user.
func NewListReactionsParameters() ListReactionsParameters {
return ListReactionsParameters{
User: DEFAULT_REACTIONS_USER,
Count: DEFAULT_REACTIONS_COUNT,
Page: DEFAULT_REACTIONS_PAGE,
Full: DEFAULT_REACTIONS_FULL,
}
}
type listReactionsResponseFull struct {
Items []struct {
Type string
Channel string
M struct {
*Message
} `json:"message"`
F struct {
*File
Reactions []ItemReaction
} `json:"file"`
FC struct {
*Comment
Reactions []ItemReaction
} `json:"comment"`
}
Paging `json:"paging"`
SlackResponse
}
func (res listReactionsResponseFull) extractReactedItems() []ReactedItem {
items := make([]ReactedItem, len(res.Items))
for i, input := range res.Items {
item := ReactedItem{}
item.Type = input.Type
switch input.Type {
case "message":
item.Channel = input.Channel
item.Message = input.M.Message
item.Reactions = input.M.Reactions
case "file":
item.File = input.F.File
item.Reactions = input.F.Reactions
case "file_comment":
item.File = input.F.File
item.Comment = input.FC.Comment
item.Reactions = input.FC.Reactions
}
items[i] = item
}
return items
}
// AddReaction adds a reaction emoji to a message, file or file comment.
func (api *Client) AddReaction(name string, item ItemRef) error {
values := url.Values{
"token": {api.config.token},
}
if name != "" {
values.Set("name", name)
}
if item.Channel != "" {
values.Set("channel", string(item.Channel))
}
if item.Timestamp != "" {
values.Set("timestamp", string(item.Timestamp))
}
if item.File != "" {
values.Set("file", string(item.File))
}
if item.Comment != "" {
values.Set("file_comment", string(item.Comment))
}
response := &SlackResponse{}
if err := post("reactions.add", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
}
// RemoveReaction removes a reaction emoji from a message, file or file comment.
func (api *Client) RemoveReaction(name string, item ItemRef) error {
values := url.Values{
"token": {api.config.token},
}
if name != "" {
values.Set("name", name)
}
if item.Channel != "" {
values.Set("channel", string(item.Channel))
}
if item.Timestamp != "" {
values.Set("timestamp", string(item.Timestamp))
}
if item.File != "" {
values.Set("file", string(item.File))
}
if item.Comment != "" {
values.Set("file_comment", string(item.Comment))
}
response := &SlackResponse{}
if err := post("reactions.remove", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
}
// GetReactions returns details about the reactions on an item.
func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) {
values := url.Values{
"token": {api.config.token},
}
if item.Channel != "" {
values.Set("channel", string(item.Channel))
}
if item.Timestamp != "" {
values.Set("timestamp", string(item.Timestamp))
}
if item.File != "" {
values.Set("file", string(item.File))
}
if item.Comment != "" {
values.Set("file_comment", string(item.Comment))
}
if params.Full != DEFAULT_REACTIONS_FULL {
values.Set("full", strconv.FormatBool(params.Full))
}
response := &getReactionsResponseFull{}
if err := post("reactions.get", values, response, api.debug); err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response.extractReactions(), nil
}
// ListReactions returns information about the items a user reacted to.
func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, *Paging, error) {
values := url.Values{
"token": {api.config.token},
}
if params.User != DEFAULT_REACTIONS_USER {
values.Add("user", params.User)
}
if params.Count != DEFAULT_REACTIONS_COUNT {
values.Add("count", strconv.Itoa(params.Count))
}
if params.Page != DEFAULT_REACTIONS_PAGE {
values.Add("page", strconv.Itoa(params.Page))
}
if params.Full != DEFAULT_REACTIONS_FULL {
values.Add("full", strconv.FormatBool(params.Full))
}
response := &listReactionsResponseFull{}
err := post("reactions.list", values, response, api.debug)
if err != nil {
return nil, nil, err
}
if !response.Ok {
return nil, nil, errors.New(response.Error)
}
return response.extractReactedItems(), &response.Paging, nil
}

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

@ -0,0 +1,39 @@
package slack
import (
"fmt"
"net/url"
)
// StartRTM calls the "rtm.start" endpoint and returns the provided URL and the full Info
// block.
//
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()`
// on it.
func (api *Client) StartRTM() (info *Info, websocketURL string, err error) {
response := &infoResponseFull{}
err = post("rtm.start", url.Values{"token": {api.config.token}}, response, api.debug)
if err != nil {
return nil, "", fmt.Errorf("post: %s", err)
}
if !response.Ok {
return nil, "", response.Error
}
// websocket.Dial does not accept url without the port (yet)
// Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3
// but slack returns the address with no port, so we have to fix it
api.Debugln("Using URL:", response.Info.URL)
websocketURL, err = websocketizeURLPort(response.Info.URL)
if err != nil {
return nil, "", fmt.Errorf("parsing response URL: %s", err)
}
return &response.Info, websocketURL, nil
}
// NewRTM returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol./
func (api *Client) NewRTM() *RTM {
return newRTM(api)
}

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

@ -0,0 +1,137 @@
package slack
import (
"errors"
"net/url"
"strconv"
)
const (
DEFAULT_SEARCH_SORT = "score"
DEFAULT_SEARCH_SORT_DIR = "desc"
DEFAULT_SEARCH_HIGHLIGHT = false
DEFAULT_SEARCH_COUNT = 100
DEFAULT_SEARCH_PAGE = 1
)
type SearchParameters struct {
Sort string
SortDirection string
Highlight bool
Count int
Page int
}
type CtxChannel struct {
ID string `json:"id"`
Name string `json:"name"`
}
type CtxMessage struct {
User string `json:"user"`
Username string `json:"username"`
Text string `json:"text"`
Timestamp string `json:"ts"`
Type string `json:"type"`
}
type SearchMessage struct {
Type string `json:"type"`
Channel CtxChannel `json:"channel"`
User string `json:"user"`
Username string `json:"username"`
Timestamp string `json:"ts"`
Text string `json:"text"`
Permalink string `json:"permalink"`
Previous CtxMessage `json:"previous"`
Previous2 CtxMessage `json:"previous_2"`
Next CtxMessage `json:"next"`
Next2 CtxMessage `json:"next_2"`
}
type SearchMessages struct {
Matches []SearchMessage `json:"matches"`
Paging `json:"paging"`
Pagination `json:"pagination"`
Total int `json:"total"`
}
type SearchFiles struct {
Matches []File `json:"matches"`
Paging `json:"paging"`
Pagination `json:"pagination"`
Total int `json:"total"`
}
type searchResponseFull struct {
Query string `json:"query"`
SearchMessages `json:"messages"`
SearchFiles `json:"files"`
SlackResponse
}
func NewSearchParameters() SearchParameters {
return SearchParameters{
Sort: DEFAULT_SEARCH_SORT,
SortDirection: DEFAULT_SEARCH_SORT_DIR,
Highlight: DEFAULT_SEARCH_HIGHLIGHT,
Count: DEFAULT_SEARCH_COUNT,
Page: DEFAULT_SEARCH_PAGE,
}
}
func (api *Client) _search(path, query string, params SearchParameters, files, messages bool) (response *searchResponseFull, error error) {
values := url.Values{
"token": {api.config.token},
"query": {query},
}
if params.Sort != DEFAULT_SEARCH_SORT {
values.Add("sort", params.Sort)
}
if params.SortDirection != DEFAULT_SEARCH_SORT_DIR {
values.Add("sort_dir", params.SortDirection)
}
if params.Highlight != DEFAULT_SEARCH_HIGHLIGHT {
values.Add("highlight", strconv.Itoa(1))
}
if params.Count != DEFAULT_SEARCH_COUNT {
values.Add("count", strconv.Itoa(params.Count))
}
if params.Page != DEFAULT_SEARCH_PAGE {
values.Add("page", strconv.Itoa(params.Page))
}
response = &searchResponseFull{}
err := post(path, values, response, api.debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
func (api *Client) Search(query string, params SearchParameters) (*SearchMessages, *SearchFiles, error) {
response, err := api._search("search.all", query, params, true, true)
if err != nil {
return nil, nil, err
}
return &response.SearchMessages, &response.SearchFiles, nil
}
func (api *Client) SearchFiles(query string, params SearchParameters) (*SearchFiles, error) {
response, err := api._search("search.files", query, params, true, false)
if err != nil {
return nil, err
}
return &response.SearchFiles, nil
}
func (api *Client) SearchMessages(query string, params SearchParameters) (*SearchMessages, error) {
response, err := api._search("search.messages", query, params, false, true)
if err != nil {
return nil, err
}
return &response.SearchMessages, nil
}

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

@ -0,0 +1,88 @@
package slack
import (
"errors"
"log"
"net/url"
"os"
)
var logger *log.Logger // A logger that can be set by consumers
/*
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"
type SlackResponse struct {
Ok bool `json:"ok"`
Error string `json:"error"`
}
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 {
config struct {
token string
}
info Info
debug bool
}
// 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 *log.Logger) {
logger = l
}
func New(token string) *Client {
s := &Client{}
s.config.token = token
return s
}
// AuthTest tests if the user is able to do authenticated requests or not
func (api *Client) AuthTest() (response *AuthTestResponse, error error) {
responseFull := &authTestResponseFull{}
err := post("auth.test", url.Values{"token": {api.config.token}}, responseFull, api.debug)
if err != nil {
return nil, err
}
if !responseFull.Ok {
return nil, errors.New(responseFull.Error)
}
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 {
logger = log.New(os.Stdout, "nlopes/slack", log.LstdFlags | log.Lshortfile)
}
}
func (api *Client) Debugf(format string, v ...interface{}) {
if api.debug {
logger.Printf(format, v...)
}
}
func (api *Client) Debugln(v ...interface{}) {
if api.debug {
logger.Println(v...)
}
}

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

@ -0,0 +1,135 @@
package slack
import (
"errors"
"net/url"
"strconv"
)
const (
DEFAULT_STARS_USER = ""
DEFAULT_STARS_COUNT = 100
DEFAULT_STARS_PAGE = 1
)
type StarsParameters struct {
User string
Count int
Page int
}
type StarredItem Item
type listResponseFull struct {
Items []Item `json:"items"`
Paging `json:"paging"`
SlackResponse
}
// NewStarsParameters initialises StarsParameters with default values
func NewStarsParameters() StarsParameters {
return StarsParameters{
User: DEFAULT_STARS_USER,
Count: DEFAULT_STARS_COUNT,
Page: DEFAULT_STARS_PAGE,
}
}
// AddStar stars an item in a channel
func (api *Client) AddStar(channel string, item ItemRef) error {
values := url.Values{
"channel": {channel},
"token": {api.config.token},
}
if item.Timestamp != "" {
values.Set("timestamp", string(item.Timestamp))
}
if item.File != "" {
values.Set("file", string(item.File))
}
if item.Comment != "" {
values.Set("file_comment", string(item.Comment))
}
response := &SlackResponse{}
if err := post("stars.add", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
}
// RemoveStar removes a starred item from a channel
func (api *Client) RemoveStar(channel string, item ItemRef) error {
values := url.Values{
"channel": {channel},
"token": {api.config.token},
}
if item.Timestamp != "" {
values.Set("timestamp", string(item.Timestamp))
}
if item.File != "" {
values.Set("file", string(item.File))
}
if item.Comment != "" {
values.Set("file_comment", string(item.Comment))
}
response := &SlackResponse{}
if err := post("stars.remove", values, response, api.debug); err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
}
// ListStars returns information about the stars a user added
func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) {
values := url.Values{
"token": {api.config.token},
}
if params.User != DEFAULT_STARS_USER {
values.Add("user", params.User)
}
if params.Count != DEFAULT_STARS_COUNT {
values.Add("count", strconv.Itoa(params.Count))
}
if params.Page != DEFAULT_STARS_PAGE {
values.Add("page", strconv.Itoa(params.Page))
}
response := &listResponseFull{}
err := post("stars.list", values, response, api.debug)
if err != nil {
return nil, nil, err
}
if !response.Ok {
return nil, nil, errors.New(response.Error)
}
return response.Items, &response.Paging, nil
}
// GetStarred returns a list of StarredItem items. The user then has to iterate over them and figure out what they should
// be looking at according to what is in the Type.
// for _, item := range items {
// switch c.Type {
// case "file_comment":
// log.Println(c.Comment)
// case "file":
// ...
//
// }
// This function still exists to maintain backwards compatibility.
// I exposed it as returning []StarredItem, so it shall stay as StarredItem
func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, *Paging, error) {
items, paging, err := api.ListStars(params)
if err != nil {
return nil, nil, err
}
starredItems := make([]StarredItem, len(items))
for i, item := range items {
starredItems[i] = StarredItem(item)
}
return starredItems, paging, nil
}

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

@ -0,0 +1,46 @@
package slack
import (
"errors"
"net/url"
)
type TeamResponse struct {
Team TeamInfo `json:"team"`
SlackResponse
}
type TeamInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Domain string `json:"domain"`
EmailDomain string `json:"email_domain"`
Icon map[string]interface{} `json:"icon"`
}
func teamRequest(path string, values url.Values, debug bool) (*TeamResponse, error) {
response := &TeamResponse{}
err := post(path, values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// GetTeamInfo gets the Team Information of the user
func (api *Client) GetTeamInfo() (*TeamInfo, error) {
values := url.Values{
"token": {api.config.token},
}
response, err := teamRequest("team.info", values, api.debug)
if err != nil {
return nil, err
}
return &response.Team, nil
}

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

@ -0,0 +1,140 @@
package slack
import (
"errors"
"net/url"
)
// UserProfile contains all the information details of a given user
type UserProfile struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
RealName string `json:"real_name"`
RealNameNormalized string `json:"real_name_normalized"`
Email string `json:"email"`
Skype string `json:"skype"`
Phone string `json:"phone"`
Image24 string `json:"image_24"`
Image32 string `json:"image_32"`
Image48 string `json:"image_48"`
Image72 string `json:"image_72"`
Image192 string `json:"image_192"`
ImageOriginal string `json:"image_original"`
Title string `json:"title"`
}
// User contains all the information of a user
type User struct {
ID string `json:"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"`
Has2FA bool `json:"has_2fa"`
HasFiles bool `json:"has_files"`
Presence string `json:"presence"`
}
// UserPresence contains details about a user online status
type UserPresence struct {
Presence string `json:"presence,omitempty"`
Online bool `json:"online,omitempty"`
AutoAway bool `json:"auto_away,omitempty"`
ManualAway bool `json:"manual_away,omitempty"`
ConnectionCount int `json:"connection_count,omitempty"`
LastActivity JSONTime `json:"last_activity,omitempty"`
}
type userResponseFull struct {
Members []User `json:"members,omitempty"` // ListUsers
User `json:"user,omitempty"` // GetUserInfo
UserPresence // GetUserPresence
SlackResponse
}
func userRequest(path string, values url.Values, debug bool) (*userResponseFull, error) {
response := &userResponseFull{}
err := post(path, values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// GetUserPresence will retrieve the current presence status of given user.
func (api *Client) GetUserPresence(user string) (*UserPresence, error) {
values := url.Values{
"token": {api.config.token},
"user": {user},
}
response, err := userRequest("users.getPresence", values, api.debug)
if err != nil {
return nil, err
}
return &response.UserPresence, nil
}
// GetUserInfo will retrive the complete user information
func (api *Client) GetUserInfo(user string) (*User, error) {
values := url.Values{
"token": {api.config.token},
"user": {user},
}
response, err := userRequest("users.info", values, api.debug)
if err != nil {
return nil, err
}
return &response.User, nil
}
// GetUsers returns the list of users (with their detailed information)
func (api *Client) GetUsers() ([]User, error) {
values := url.Values{
"token": {api.config.token},
"presence": {"1"},
}
response, err := userRequest("users.list", values, api.debug)
if err != nil {
return nil, err
}
return response.Members, nil
}
// SetUserAsActive marks the currently authenticated user as active
func (api *Client) SetUserAsActive() error {
values := url.Values{
"token": {api.config.token},
}
_, err := userRequest("users.setActive", values, api.debug)
if err != nil {
return err
}
return nil
}
// SetUserPresence changes the currently authenticated user presence
func (api *Client) SetUserPresence(presence string) error {
values := url.Values{
"token": {api.config.token},
"presence": {presence},
}
_, err := userRequest("users.setPresence", values, api.debug)
if err != nil {
return err
}
return nil
}

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

@ -0,0 +1,93 @@
package slack
import (
"encoding/json"
"errors"
"time"
"golang.org/x/net/websocket"
)
const (
// MaxMessageTextLength is the current maximum message length in number of characters as defined here
// https://api.slack.com/rtm#limits
MaxMessageTextLength = 4000
)
// RTM represents a managed websocket connection. It also supports
// all the methods of the `Client` type.
//
// Create this element with Client's NewRTM().
type RTM struct {
idGen IDGenerator
pings map[int]time.Time
// Connection life-cycle
conn *websocket.Conn
IncomingEvents chan RTMEvent
outgoingMessages chan OutgoingMessage
killChannel chan bool
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
}
// NewRTM returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol.
func newRTM(api *Client) *RTM {
return &RTM{
Client: *api,
IncomingEvents: make(chan RTMEvent, 50),
outgoingMessages: make(chan OutgoingMessage, 20),
pings: make(map[int]time.Time),
isConnected: false,
wasIntentional: true,
killChannel: make(chan bool),
forcePing: make(chan bool),
rawEvents: make(chan json.RawMessage),
idGen: NewSafeID(1),
}
}
// Disconnect and wait, blocking until a successful disconnection.
func (rtm *RTM) Disconnect() error {
if !rtm.isConnected {
return errors.New("Invalid call to Disconnect - Slack API is already disconnected")
}
rtm.killChannel <- true
return nil
}
// Reconnect only makes sense if you've successfully disconnectd with Disconnect().
func (rtm *RTM) Reconnect() error {
logger.Println("RTM::Reconnect not implemented!")
return nil
}
// GetInfo returns the info structure received when calling
// "startrtm", holding all channels, groups and other metadata needed
// to implement a full chat client. It will be non-nil after a call to
// StartRTM().
func (rtm *RTM) GetInfo() *Info {
return rtm.info
}
// SendMessage submits a simple message through the websocket. For
// more complicated messages, use `rtm.PostMessage` with a complete
// struct describing your attachments and all.
func (rtm *RTM) SendMessage(msg *OutgoingMessage) {
if msg == nil {
rtm.Debugln("Error: Attempted to SendMessage(nil)")
return
}
rtm.outgoingMessages <- *msg
}

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

@ -0,0 +1,72 @@
package slack
// ChannelCreatedEvent represents the Channel created event
type ChannelCreatedEvent struct {
Type string `json:"type"`
Channel ChannelCreatedInfo `json:"channel"`
EventTimestamp string `json:"event_ts"`
}
// ChannelCreatedInfo represents the information associated with the Channel created event
type ChannelCreatedInfo struct {
ID string `json:"id"`
IsChannel bool `json:"is_channel"`
Name string `json:"name"`
Created int `json:"created"`
Creator string `json:"creator"`
}
// ChannelJoinedEvent represents the Channel joined event
type ChannelJoinedEvent struct {
Type string `json:"type"`
Channel Channel `json:"channel"`
}
// ChannelInfoEvent represents the Channel info event
type ChannelInfoEvent struct {
// channel_left
// channel_deleted
// channel_archive
// channel_unarchive
Type string `json:"type"`
Channel string `json:"channel"`
User string `json:"user,omitempty"`
Timestamp string `json:"ts,omitempty"`
}
// ChannelRenameEvent represents the Channel rename event
type ChannelRenameEvent struct {
Type string `json:"type"`
Channel ChannelRenameInfo `json:"channel"`
Timestamp string `json:"event_ts"`
}
// ChannelRenameInfo represents the information associated with a Channel rename event
type ChannelRenameInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Created string `json:"created"`
}
// ChannelHistoryChangedEvent represents the Channel history changed event
type ChannelHistoryChangedEvent struct {
Type string `json:"type"`
Latest string `json:"latest"`
Timestamp string `json:"ts"`
EventTimestamp string `json:"event_ts"`
}
// ChannelMarkedEvent represents the Channel marked event
type ChannelMarkedEvent ChannelInfoEvent
// ChannelLeftEvent represents the Channel left event
type ChannelLeftEvent ChannelInfoEvent
// ChannelDeletedEvent represents the Channel deleted event
type ChannelDeletedEvent ChannelInfoEvent
// ChannelArchiveEvent represents the Channel archive event
type ChannelArchiveEvent ChannelInfoEvent
// ChannelUnarchiveEvent represents the Channel unarchive event
type ChannelUnarchiveEvent ChannelInfoEvent

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

@ -0,0 +1,23 @@
package slack
// IMCreatedEvent represents the IM created event
type IMCreatedEvent struct {
Type string `json:"type"`
User string `json:"user"`
Channel ChannelCreatedInfo `json:"channel"`
}
// IMHistoryChangedEvent represents the IM history changed event
type IMHistoryChangedEvent ChannelHistoryChangedEvent
// IMOpenEvent represents the IM open event
type IMOpenEvent ChannelInfoEvent
// IMCloseEvent represents the IM close event
type IMCloseEvent ChannelInfoEvent
// IMMarkedEvent represents the IM marked event
type IMMarkedEvent ChannelInfoEvent
// IMMarkedHistoryChanged represents the IM marked history changed event
type IMMarkedHistoryChanged ChannelInfoEvent

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

@ -0,0 +1,8 @@
package slack
// DNDUpdatedEvent represents the update event for Do Not Disturb
type DNDUpdatedEvent struct {
Type string `json:"type"`
User string `json:"user"`
Status DNDStatus `json:"dnd_status"`
}

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

@ -0,0 +1,49 @@
package slack
// FileActionEvent represents the File action event
type fileActionEvent struct {
Type string `json:"type"`
EventTimestamp string `json:"event_ts"`
File File `json:"file"`
// FileID is used for FileDeletedEvent
FileID string `json:"file_id,omitempty"`
}
// FileCreatedEvent represents the File created event
type FileCreatedEvent fileActionEvent
// FileSharedEvent represents the File shared event
type FileSharedEvent fileActionEvent
// FilePublicEvent represents the File public event
type FilePublicEvent fileActionEvent
// FileUnsharedEvent represents the File unshared event
type FileUnsharedEvent fileActionEvent
// FileChangeEvent represents the File change event
type FileChangeEvent fileActionEvent
// FileDeletedEvent represents the File deleted event
type FileDeletedEvent fileActionEvent
// FilePrivateEvent represents the File private event
type FilePrivateEvent fileActionEvent
// FileCommentAddedEvent represents the File comment added event
type FileCommentAddedEvent struct {
fileActionEvent
Comment Comment `json:"comment"`
}
// FileCommentEditedEvent represents the File comment edited event
type FileCommentEditedEvent struct {
fileActionEvent
Comment Comment `json:"comment"`
}
// FileCommentDeletedEvent represents the File comment deleted event
type FileCommentDeletedEvent struct {
fileActionEvent
Comment string `json:"comment"`
}

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