4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-06-26 17:49:23 +00:00

Compare commits

...

47 Commits

Author SHA1 Message Date
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
Wim
30f30364d5 Release v0.5.0 2016-07-27 22:42:59 +02:00
Wim
073d90da88 Fix docker build 2016-07-23 00:03:54 +02:00
Wim
c769e23a9a Fix crash on invalid team 2016-07-22 23:47:25 +02:00
Wim
9db48f4794 Update readme 2016-07-22 23:36:54 +02:00
Wim
911c597377 Sync with mattermost 3.2.0 2016-07-22 23:15:59 +02:00
Wim
28244ffd9a Fix pointer reuse problem 2016-07-22 23:04:08 +02:00
Wim
3e38c7945c Actually add sasl.go 2016-07-22 22:51:11 +02:00
Wim
79ffb76f6e Add (PLAIN) SASL support 2016-07-21 23:47:44 +02:00
Wim
5fe4b749cf Do not check bindaddress when not using the server 2016-07-17 22:15:19 +02:00
Wim
6991d85da9 Add FAQ section 2016-07-15 21:59:14 +02:00
Wim
c1c187a1ab Fix markdown 2016-07-12 22:00:38 +02:00
Wim
055d12e3ef Release v0.5.0-beta1 2016-07-12 21:32:15 +02:00
Wim
b49429d722 Add migration info 2016-07-12 01:23:36 +02:00
Wim
815c7f8d64 Update version 2016-07-12 01:07:37 +02:00
Wim
c879f79456 Update documentation 2016-07-12 01:02:56 +02:00
Wim
3bc25f4707 Update documentation 2016-07-12 00:25:32 +02:00
Wim
300cfe044a Remove token check 2016-07-12 00:23:36 +02:00
Wim
fb586f4a96 Remove Port from IRC config. Specify it with server 2016-07-11 23:30:42 +02:00
Wim
ced371bece Add port to BindAddress 2016-07-11 23:22:56 +02:00
Wim
a87cac1982 Remove multiple Token config. Use same channel setup as from matterbridge-plus 2016-07-11 22:55:58 +02:00
Wim
8fb5c7afa6 Remove UseSlackCircumfix. Use RemoteNickFormat 2016-07-11 21:26:13 +02:00
Wim
aceb830378 Converge with matterbridge-plus 2016-07-11 21:23:33 +02:00
73 changed files with 6330 additions and 327 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 \
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

128
README.md
View File

@ -1,14 +1,40 @@
# matterbridge
Simple bridge between mattermost and IRC. Uses the in/outgoing webhooks.
Relays public channel messages between mattermost and IRC.
Simple bridge between mattermost, IRC, XMPP and Gitter
Requires mattermost 1.2.0+
* 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.
There is also [matterbridge-plus] (https://github.com/42wim/matterbridge-plus) which uses the mattermost API and needs a dedicated user (bot). But requires no incoming/outgoing webhook setup.
Look at [matterbridge.conf.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.conf.sample) for documentation and an example.
## Changelog
Since v0.6.0-beta support for XMPP and Gitter is added. More details in [changelog.md] (https://github.com/42wim/matterbridge/blob/master/changelog.md)
## Requirements:
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.4.2)
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/)
* For use with mattermost 3.3.0 [v0.6.0-beta1](https://github.com/42wim/matterircd/releases/tag/v0.6.0-beta1)
* 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.0 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)
@ -31,77 +57,26 @@ matterbridge
3) Now you can run matterbridge.
```
Usage of matterbridge:
-conf="matterbridge.conf": config file
Usage of ./matterbridge:
-conf string
config file (default "matterbridge.conf")
-debug
enable debug
-plus
running using API instead of webhooks (deprecated, set Plus flag in [general] config)
-version
show version
```
Matterbridge will:
* start a webserver listening on the port specified in the configuration.
* connect to specified irc server and channel.
* send messages from mattermost to irc and vice versa, messages in mattermost will appear with irc-nick
## config
### matterbridge
matterbridge looks for matterbridge.conf in current directory. (use -conf to specify another file)
Look at matterbridge.conf.sample for an example
```
[IRC]
server="irc.freenode.net"
port=6667
UseTLS=false
SkipTLSVerify=true
nick="matterbot"
channel="#matterbridge"
UseSlackCircumfix=false
#Freenode nickserv
NickServNick="nickserv"
#Password for nickserv
NickServPassword="secret"
#Ignore the messages from these nicks. They will not be sent to mattermost
IgnoreNicks="ircspammer1 ircspammer2"
[mattermost]
#url is your incoming webhook url (account settings - integrations - incoming webhooks)
url="http://mattermost.yourdomain.com/hooks/incomingwebhookkey"
#port the bridge webserver will listen on
port=9999
#address the webserver will bind to
BindAddress="0.0.0.0"
showjoinpart=true #show irc users joining and parting
#the token you get from the outgoing webhook in mattermost. If empty no token check will be done.
#if you use multiple IRC channel (see below, this must be empty!)
token=yourtokenfrommattermost
#disable certificate checking (selfsigned certificates)
#SkipTLSVerify=true
#whether to prefix messages from IRC to mattermost with the sender's nick. Useful if username overrides for incoming webhooks isn't enabled on the mattermost server
PrefixMessagesWithNick=false
#how to format the list of IRC nicks when displayed in mattermost. Possible options are "table" and "plain"
NickFormatter=plain
#how many nicks to list per row for formatters that support this
NicksPerRow=4
#Ignore the messages from these nicks. They will not be sent to irc
IgnoreNicks="mmbot spammer2"
#multiple channel config
#token you can find in your outgoing webhook
[Token "outgoingwebhooktoken1"]
IRCChannel="#off-topic"
MMChannel="off-topic"
[Token "outgoingwebhooktoken2"]
IRCChannel="#testing"
MMChannel="testing"
[general]
#request your API key on https://github.com/giphy/GiphyAPI. This is a public beta key
GiphyApiKey="dc6zaTOxFJmzC"
```
Look at [matterbridge.conf.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.conf.sample) for an example.
### mattermost
You'll have to configure the incoming en outgoing webhooks.
#### webhooks version
You'll have to configure the incoming and outgoing webhooks.
* incoming webhooks
Go to "account settings" - integrations - "incoming webhooks".
@ -112,5 +87,20 @@ This URL should be set in the matterbridge.conf in the [mattermost] section (see
Go to "account settings" - integrations - "outgoing webhooks".
Choose a channel (the same as the one from incoming webhooks) and fill in the address and port of the server matterbridge will run on.
e.g. http://192.168.1.1:9999 (9999 is the port specified in [mattermost] section of matterbridge.conf)
e.g. http://192.168.1.1:9999 (192.168.1.1:9999 is the BindAddress specified in [mattermost] section of matterbridge.conf)
#### plus version
You'll have to create a new dedicated user on your mattermost instance.
Specify the login and password in [mattermost] section of matterbridge.conf
## FAQ
Please look at [matterbridge.conf.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.conf.sample) for more information first.
### Mattermost doesn't show the IRC nicks
If you're running the webhooks version, this can be fixed by either:
* enabling "override usernames". See [mattermost documentation](http://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks)
* setting ```PrefixMessagesWithNick``` to ```true``` in ```mattermost``` section of your matterbridge.conf.
If you're running the plus version you'll need to:
* setting ```PrefixMessagesWithNick``` to ```true``` in ```mattermost``` section of your matterbridge.conf.
Also look at the ```RemoteNickFormat``` setting.

142
bridge/bridge.go Normal file
View File

@ -0,0 +1,142 @@
package bridge
import (
//"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/xmpp"
log "github.com/Sirupsen/logrus"
"strings"
)
type Bridge struct {
*config.Config
Source string
Bridges []Bridger
Channels []map[string]string
ignoreNicks map[string][]string
}
type Bridger interface {
Send(msg config.Message) error
Name() string
Connect() error
//Command(cmd string) string
}
func NewBridge(cfg *config.Config) error {
c := make(chan config.Message)
b := &Bridge{}
b.Config = cfg
if cfg.IRC.Enable {
b.Bridges = append(b.Bridges, birc.New(cfg, c))
}
if cfg.Mattermost.Enable {
b.Bridges = append(b.Bridges, bmattermost.New(cfg, c))
}
if cfg.Xmpp.Enable {
b.Bridges = append(b.Bridges, bxmpp.New(cfg, c))
}
if cfg.Gitter.Enable {
b.Bridges = append(b.Bridges, bgitter.New(cfg, c))
}
if len(b.Bridges) < 2 {
log.Fatalf("only %d sections enabled. Need at least 2 sections enabled (eg [IRC] and [mattermost]", len(b.Bridges))
}
for _, br := range b.Bridges {
br.Connect()
}
b.mapChannels()
b.mapIgnores()
b.handleReceive(c)
return nil
}
func (b *Bridge) handleReceive(c chan config.Message) {
for {
select {
case msg := <-c:
for _, br := range b.Bridges {
b.handleMessage(msg, br)
}
}
}
}
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
b.Channels = append(b.Channels, m)
}
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)
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]
}
}
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())
dest.Send(msg)
}
}
func (b *Bridge) ignoreMessage(msg *config.Message) bool {
// should we discard messages ?
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)
}
}

94
bridge/config/config.go Normal file
View File

@ -0,0 +1,94 @@
package config
import (
"gopkg.in/gcfg.v1"
"io/ioutil"
"log"
)
type Message struct {
Text string
Channel string
Username string
Origin string
}
type Config struct {
IRC struct {
UseTLS bool
UseSASL bool
SkipTLSVerify bool
Server string
Nick string
Password string
Channel string
NickServNick string
NickServPassword string
RemoteNickFormat string
IgnoreNicks string
Enable bool
}
Gitter struct {
Enable bool
IgnoreNicks string
Nick string
RemoteNickFormat string
Token string
}
Mattermost struct {
URL string
ShowJoinPart bool
IconURL string
SkipTLSVerify bool
BindAddress string
Channel string
PrefixMessagesWithNick bool
NicksPerRow int
NickFormatter string
Server string
Team string
Login string
Password string
RemoteNickFormat string
IgnoreNicks string
NoTLS bool
Enable 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
}
General struct {
GiphyAPIKey string
Xmpp bool
Irc bool
Mattermost bool
Plus bool
}
}
func NewConfig(cfgfile string) *Config {
var cfg Config
content, err := ioutil.ReadFile(cfgfile)
if err != nil {
log.Fatal(err)
}
err = gcfg.ReadStringInto(&cfg, string(content))
if err != nil {
log.Fatal("Failed to parse "+cfgfile+":", err)
}
return &cfg
}

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

59
bridge/irc/helper.go Normal file
View File

@ -0,0 +1,59 @@
package birc
import (
"strings"
)
func tableformatter(nicks []string, nicksPerRow int, continued bool) string {
result := "|IRC users"
if continued {
result = "|(continued)"
}
for i := 0; i < 2; i++ {
for j := 1; j <= nicksPerRow && j <= len(nicks); j++ {
if i == 0 {
result += "|"
} else {
result += ":-|"
}
}
result += "\r\n|"
}
result += nicks[0] + "|"
for i := 1; i < len(nicks); i++ {
if i%nicksPerRow == 0 {
result += "\r\n|" + nicks[i] + "|"
} else {
result += nicks[i] + "|"
}
}
return result
}
func plainformatter(nicks []string, nicksPerRow int) string {
return strings.Join(nicks, ", ") + " currently on IRC"
}
func IsMarkup(message string) bool {
switch message[0] {
case '|':
fallthrough
case '#':
fallthrough
case '_':
fallthrough
case '*':
fallthrough
case '~':
fallthrough
case '-':
fallthrough
case ':':
fallthrough
case '>':
fallthrough
case '=':
return true
}
return false
}

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

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

79
changelog.md Normal file
View File

@ -0,0 +1,79 @@
# 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

@ -1,39 +1,220 @@
#This is configuration for matterbridge.
###################################################################
#IRC section
###################################################################
[IRC]
server="irc.freenode.net"
port=6667
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#irc server to connect to.
#REQUIRED
Server="irc.freenode.net:6667"
#Enable to use TLS connection to your irc server.
#OPTIONAL (default false)
UseTLS=false
#Enable SASL (PLAIN) authentication. (freenode requires this from eg AWS hosts)
#It uses NickServNick and NickServPassword as login and password
#OPTIONAL (default false)
UseSASL=false
#Enable to not verify the certificate on your irc server. i
#e.g. when using selfsigned certificates
#OPTIONAL (default false)
SkipTLSVerify=true
nick="matterbot"
channel="#matterbridge"
UseSlackCircumfix=false
#NickServNick="nickserv"
#NickServPassword="secret"
#Your nick on irc.
#REQUIRED
Nick="matterbot"
#If you registered your bot with a service like Nickserv on freenode.
#Also being used when UseSASL=true
#OPTIONAL
NickServNick="nickserv"
NickServPassword="secret"
#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}>
#Nicks you want to ignore.
#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]
url="http://yourdomain/hooks/yourhookkey"
port=9999
showjoinpart=true
#remove token when using multiple channels!
token=yourtokenfrommattermost
#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.
#Url is your incoming webhook url as specified in mattermost.
#See account settings - integrations - incoming webhooks on mattermost.
#REQUIRED
URL="https://yourdomain/hooks/yourhookkey"
#Address to listen on for outgoing webhook requests from mattermost.
#See account settings - integrations - outgoing webhooks on mattermost.
#This setting will not be used when using -plus switch which doesn't use
#webhooks
#REQUIRED
BindAddress="0.0.0.0:9999"
#Icon that will be showed in mattermost.
#OPTIONAL
IconURL="http://youricon.png"
#SkipTLSVerify=true
#BindAddress="0.0.0.0"
#### Settings for matterbridge -plus
#### Thse settings will only be used when using the -plus switch.
#The mattermost hostname.
#REQUIRED
Server="yourmattermostserver.domain"
#Your team on mattermost.
#REQUIRED
Team="yourteam"
#login/pass of your bot.
#Use a dedicated user for this and not your own!
#REQUIRED
Login="yourlogin"
Password="yourpass"
#Enable this to make a http connection (instead of https) to your mattermost.
#OPTIONAL (default false)
NoTLS=false
#### Shared settings for matterbridge and -plus
#Enable to not verify the certificate on your mattermost server.
#e.g. when using selfsigned certificates
#OPTIONAL (default false)
SkipTLSVerify=true
#Enable to show IRC joins/parts in mattermost.
#OPTIONAL (default false)
ShowJoinPart=false
#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 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 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 mattermost.
#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"
[general]
GiphyAPIKey=dc6zaTOxFJmzC
###################################################################
#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}>
###################################################################
#multiple channel config
#token you can find in your outgoing webhook
[Token "outgoingwebhooktoken1"]
IRCChannel="#off-topic"
MMChannel="off-topic"
###################################################################
#You can specify multiple channels.
#The name is just an identifier for you.
#REQUIRED (at least 1 channel)
[Channel "channel1"]
#Choose the IRC channel to send messages to.
IRC="#off-topic"
#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"
[Token "outgoingwebhooktoken2"]
IRCChannel="#testing"
MMChannel="testing"
[Channel "testchannel"]
IRC="#testing"
mattermost="testing"
xmpp="testing"
gitter="user/repo"
###################################################################
#general
###################################################################
[general]
#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

@ -3,11 +3,12 @@ package main
import (
"flag"
"fmt"
"github.com/42wim/matterbridge-plus/bridge"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
)
var Version = "0.4.2"
var version = "0.6.0-beta2"
func init() {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
@ -17,9 +18,10 @@ 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 (deprecated, set Plus flag in [general] config)")
flag.Parse()
if *flagVersion {
fmt.Println("Version:", Version)
fmt.Println("version:", version)
return
}
flag.Parse()
@ -27,7 +29,13 @@ func main() {
log.Info("enabling debug")
log.SetLevel(log.DebugLevel)
}
fmt.Println("running version", Version)
bridge.NewBridge("matterbot", bridge.NewConfig(*flagConfig), "legacy")
select {}
fmt.Println("running version", version)
cfg := config.NewConfig(*flagConfig)
if *flagPlus {
cfg.General.Plus = true
}
err := bridge.NewBridge(cfg)
if err != nil {
log.Debugf("starting bridge failed %#v", err)
}
}

View File

@ -0,0 +1,643 @@
package matterclient
import (
"crypto/tls"
"encoding/json"
"errors"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"sync"
"time"
log "github.com/Sirupsen/logrus"
"github.com/gorilla/websocket"
"github.com/jpillora/backoff"
"github.com/mattermost/platform/model"
)
type Credentials struct {
Login string
Team string
Pass string
Server string
NoTLS bool
SkipTLSVerify bool
}
type Message struct {
Raw *model.WebSocketEvent
Post *model.Post
Team string
Channel string
Username string
Text string
}
type Team struct {
Team *model.Team
Id string
Channels *model.ChannelList
MoreChannels *model.ChannelList
Users map[string]*model.User
}
type MMClient struct {
sync.RWMutex
*Credentials
Team *Team
OtherTeams []*Team
Client *model.Client
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 {
cred := &Credentials{Login: login, Pass: pass, Team: team, Server: server}
mmclient := &MMClient{Credentials: cred, MessageChan: make(chan *Message, 100), Users: make(map[string]*model.User)}
mmclient.log = log.WithFields(log.Fields{"module": "matterclient"})
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
return mmclient
}
func (m *MMClient) SetLogLevel(level string) {
l, err := log.ParseLevel(level)
if err != nil {
log.SetLevel(log.InfoLevel)
return
}
log.SetLevel(l)
}
func (m *MMClient) Login() error {
m.WsConnected = false
if m.WsQuit {
return nil
}
b := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
uriScheme := "https://"
wsScheme := "wss://"
if m.NoTLS {
uriScheme = "http://"
wsScheme = "ws://"
}
// login to mattermost
m.Client = model.NewClient(uriScheme + m.Credentials.Server)
m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
var myinfo *model.Result
var appErr *model.AppError
var logmsg = "trying login"
for {
m.log.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
m.log.Debugf(logmsg+" with %s", model.SESSION_COOKIE_TOKEN)
token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=")
if len(token) != 2 {
return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken")
}
m.Client.HttpClient.Jar = m.createCookieJar(token[1])
m.Client.MockSession(token[1])
myinfo, appErr = m.Client.GetMe("")
if appErr != nil {
return errors.New(appErr.DetailedError)
}
if myinfo.Data.(*model.User) == nil {
m.log.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
return errors.New("invalid " + model.SESSION_COOKIE_TOKEN)
}
} else {
myinfo, appErr = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
}
if appErr != nil {
d := b.Duration()
m.log.Debug(appErr.DetailedError)
if !strings.Contains(appErr.DetailedError, "connection refused") &&
!strings.Contains(appErr.DetailedError, "invalid character") {
if appErr.Message == "" {
return errors.New(appErr.DetailedError)
}
return errors.New(appErr.Message)
}
m.log.Debugf("LOGIN: %s, reconnecting in %s", appErr, d)
time.Sleep(d)
logmsg = "retrying login"
continue
}
break
}
// reset timer
b.Reset()
err := m.initUser()
if err != nil {
return err
}
if m.Team == nil {
return errors.New("team not found")
}
// set our team id as default route
m.Client.SetTeamId(m.Team.Id)
// setup websocket connection
wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX + "/users/websocket"
header := http.Header{}
header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
m.log.Debug("WsClient: making connection")
for {
wsDialer := &websocket.Dialer{Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
m.WsClient, _, err = wsDialer.Dial(wsurl, header)
if err != nil {
d := b.Duration()
m.log.Debugf("WSS: %s, reconnecting in %s", err, d)
time.Sleep(d)
continue
}
break
}
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
return nil
}
func (m *MMClient) Logout() error {
m.log.Debugf("logout as %s (team: %s) on %s", m.Credentials.Login, m.Credentials.Team, m.Credentials.Server)
m.WsQuit = true
m.WsClient.Close()
m.WsClient.UnderlyingConn().Close()
m.WsClient = nil
_, err := m.Client.Logout()
if err != nil {
return err
}
return nil
}
func (m *MMClient) WsReceiver() {
for {
var rawMsg json.RawMessage
var err error
if !m.WsConnected {
continue
}
if m.WsQuit {
m.log.Debug("exiting WsReceiver")
return
}
if _, rawMsg, err = m.WsClient.ReadMessage(); err != nil {
m.log.Error("error:", err)
// reconnect
m.Login()
}
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
}
var response model.WebSocketResponse
if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
m.log.Debugf("WsReceiver: %#v", response)
m.parseResponse(response)
continue
}
}
}
func (m *MMClient) parseMessage(rmsg *Message) {
switch rmsg.Raw.Event {
case model.WEBSOCKET_EVENT_POSTED:
m.parseActionPost(rmsg)
/*
case model.ACTION_USER_REMOVED:
m.handleWsActionUserRemoved(&rmsg)
case model.ACTION_USER_ADDED:
m.handleWsActionUserAdded(&rmsg)
*/
}
}
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.Data["post"].(string)))
// we don't have the user, refresh the userlist
if m.GetUser(data.UserId) == nil {
m.UpdateUsers()
}
rmsg.Username = m.GetUser(data.UserId).Username
rmsg.Channel = m.GetChannelName(data.ChannelId)
rmsg.Team = m.GetTeamName(rmsg.Raw.TeamId)
// direct message
if rmsg.Raw.Data["channel_type"] == "D" {
rmsg.Channel = m.GetUser(data.UserId).Username
}
rmsg.Text = data.Message
rmsg.Post = data
return
}
func (m *MMClient) UpdateUsers() error {
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()
return nil
}
func (m *MMClient) UpdateChannels() error {
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)
m.Unlock()
return nil
}
func (m *MMClient) GetChannelName(channelId string) string {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
if channel.Id == channelId {
return channel.Name
}
}
}
return ""
}
func (m *MMClient) GetChannelId(name string, teamId string) string {
m.RLock()
defer m.RUnlock()
if teamId == "" {
teamId = m.Team.Id
}
for _, t := range m.OtherTeams {
if t.Id == teamId {
for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
if channel.Name == name {
return channel.Id
}
}
}
}
return ""
}
func (m *MMClient) GetChannelHeader(channelId string) string {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
if channel.Id == channelId {
return channel.Header
}
}
}
return ""
}
func (m *MMClient) PostMessage(channelId string, text string) {
post := &model.Post{ChannelId: channelId, Message: text}
m.Client.CreatePost(post)
}
func (m *MMClient) JoinChannel(channelId string) error {
m.RLock()
defer m.RUnlock()
for _, c := range m.Team.Channels.Channels {
if c.Id == channelId {
m.log.Debug("Not joining ", channelId, " already joined.")
return nil
}
}
m.log.Debug("Joining ", channelId)
_, err := m.Client.JoinChannel(channelId)
if err != nil {
return errors.New("failed to join")
}
return nil
}
func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList {
res, err := m.Client.GetPostsSince(channelId, time)
if err != nil {
return nil
}
return res.Data.(*model.PostList)
}
func (m *MMClient) SearchPosts(query string) *model.PostList {
res, err := m.Client.SearchPosts(query, false)
if err != nil {
return nil
}
return res.Data.(*model.PostList)
}
func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList {
res, err := m.Client.GetPosts(channelId, 0, limit, "")
if err != nil {
return nil
}
return res.Data.(*model.PostList)
}
func (m *MMClient) GetPublicLink(filename string) string {
res, err := m.Client.GetPublicLink(filename)
if err != nil {
return ""
}
return res.Data.(string)
}
func (m *MMClient) GetPublicLinks(filenames []string) []string {
var output []string
for _, f := range filenames {
res, err := m.Client.GetPublicLink(f)
if err != nil {
continue
}
output = append(output, res.Data.(string))
}
return output
}
func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
data := make(map[string]string)
data["channel_id"] = channelId
data["channel_header"] = header
m.log.Debugf("updating channelheader %#v, %#v", channelId, header)
_, err := m.Client.UpdateChannelHeader(data)
if err != nil {
log.Error(err)
}
}
func (m *MMClient) UpdateLastViewed(channelId string) {
m.log.Debugf("posting lastview %#v", channelId)
_, err := m.Client.UpdateLastViewedAt(channelId)
if err != nil {
m.log.Error(err)
}
}
func (m *MMClient) UsernamesInChannel(channelId string) []string {
ceiRes, err := m.Client.GetChannelExtraInfo(channelId, 5000, "")
if err != nil {
m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelId, err)
return []string{}
}
extra := ceiRes.Data.(*model.ChannelExtra)
result := []string{}
for _, member := range extra.Members {
result = append(result, member.Username)
}
return result
}
func (m *MMClient) createCookieJar(token string) *cookiejar.Jar {
var cookies []*http.Cookie
jar, _ := cookiejar.New(nil)
firstCookie := &http.Cookie{
Name: "MMAUTHTOKEN",
Value: token,
Path: "/",
Domain: m.Credentials.Server,
}
cookies = append(cookies, firstCookie)
cookieURL, _ := url.Parse("https://" + m.Credentials.Server)
jar.SetCookies(cookieURL, cookies)
return jar
}
// SendDirectMessage sends a direct message to specified user
func (m *MMClient) SendDirectMessage(toUserId string, msg string) {
m.log.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
// create DM channel (only happens on first message)
_, err := m.Client.CreateDirectChannel(toUserId)
if err != nil {
m.log.Debugf("SendDirectMessage to %#v failed: %s", toUserId, err)
}
channelName := model.GetDMNameFromIds(toUserId, m.User.Id)
// update our channels
mmchannels, _ := m.Client.GetChannels("")
m.Lock()
m.Team.Channels = mmchannels.Data.(*model.ChannelList)
m.Unlock()
// build & send the message
msg = strings.Replace(msg, "\r", "", -1)
post := &model.Post{ChannelId: m.GetChannelId(channelName, ""), Message: msg}
m.Client.CreatePost(post)
}
// GetTeamName returns the name of the specified teamId
func (m *MMClient) GetTeamName(teamId string) string {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
if t.Id == teamId {
return t.Team.Name
}
}
return ""
}
// GetChannels returns all channels we're members off
func (m *MMClient) GetChannels() []*model.Channel {
m.RLock()
defer m.RUnlock()
var channels []*model.Channel
// our primary team channels first
channels = append(channels, m.Team.Channels.Channels...)
for _, t := range m.OtherTeams {
if t.Id != m.Team.Id {
channels = append(channels, t.Channels.Channels...)
}
}
return channels
}
// GetMoreChannels returns existing channels where we're not a member off.
func (m *MMClient) GetMoreChannels() []*model.Channel {
m.RLock()
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.MoreChannels.Channels...)
}
return channels
}
// GetTeamFromChannel returns teamId belonging to channel (DM channels have no teamId).
func (m *MMClient) GetTeamFromChannel(channelId string) string {
m.RLock()
defer m.RUnlock()
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
}
}
}
return ""
}
func (m *MMClient) GetLastViewedAt(channelId string) int64 {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
if _, ok := t.Channels.Members[channelId]; ok {
return t.Channels.Members[channelId].LastViewedAt
}
}
return 0
}
func (m *MMClient) GetUsers() map[string]*model.User {
users := make(map[string]*model.User)
m.RLock()
defer m.RUnlock()
for k, v := range m.Users {
users[k] = v
}
return users
}
func (m *MMClient) GetUser(userId string) *model.User {
m.RLock()
defer m.RUnlock()
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()
defer m.Unlock()
m.log.Debug("initUser()")
initLoad, err := m.Client.GetInitialLoad()
if err != nil {
return err
}
initData := initLoad.Data.(*model.InitialLoad)
m.User = initData.User
// we only load all team data on initial login.
// all other updates are for channels from our (primary) team only.
m.log.Debug("initUser(): loading all team data")
for _, v := range initData.Teams {
m.Client.SetTeamId(v.Id)
mmusers, _ := m.Client.GetProfiles(v.Id, "")
t := &Team{Team: v, Users: mmusers.Data.(map[string]*model.User), Id: v.Id}
mmchannels, _ := m.Client.GetChannels("")
t.Channels = mmchannels.Data.(*model.ChannelList)
mmchannels, _ = m.Client.GetMoreChannels("")
t.MoreChannels = mmchannels.Data.(*model.ChannelList)
m.OtherTeams = append(m.OtherTeams, t)
if v.Name == m.Credentials.Team {
m.Team = t
m.log.Debugf("initUser(): found our team %s (id: %s)", v.Name, v.Id)
}
// add all users
for k, v := range t.Users {
m.Users[k] = v
}
}
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

@ -10,8 +10,8 @@ import (
"io"
"io/ioutil"
"log"
"net"
"net/http"
"strconv"
)
// OMessage for mattermost incoming webhook. (send to mattermost)
@ -51,7 +51,6 @@ type Client struct {
// Config for client.
type Config struct {
Port int // Port to listen on.
BindAddress string // Address to listen on
Token string // Only allow this token from Mattermost. (Allow everything when empty)
InsecureSkipVerify bool // disable certificate checking
@ -61,15 +60,15 @@ type Config struct {
// New Mattermost client.
func New(url string, config Config) *Client {
c := &Client{Url: url, In: make(chan IMessage), Out: make(chan OMessage), Config: config}
if c.Port == 0 {
c.Port = 9999
}
c.BindAddress += ":"
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
}
c.httpclient = &http.Client{Transport: tr}
if !c.DisableServer {
_, _, err := net.SplitHostPort(c.BindAddress)
if err != nil {
log.Fatalf("incorrect bindaddress %s", c.BindAddress)
}
go c.StartServer()
}
return c
@ -79,8 +78,8 @@ func New(url string, config Config) *Client {
func (c *Client) StartServer() {
mux := http.NewServeMux()
mux.Handle("/", c)
log.Printf("Listening on http://%v:%v...\n", c.BindAddress, c.Port)
if err := http.ListenAndServe((c.BindAddress + strconv.Itoa(c.Port)), mux); err != nil {
log.Printf("Listening on http://%v...\n", c.BindAddress)
if err := http.ListenAndServe(c.BindAddress, mux); err != nil {
log.Fatal(err)
}
}

50
migration.md Normal file
View File

@ -0,0 +1,50 @@
# 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

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

@ -0,0 +1,22 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import (
"github.com/mattermost/platform/model"
)
type EmojiInterface interface {
CanUserCreateEmoji(string, []*model.TeamMember) bool
}
var theEmojiInterface EmojiInterface
func RegisterEmojiInterface(newInterface EmojiInterface) {
theEmojiInterface = newInterface
}
func GetEmojiInterface() EmojiInterface {
return theEmojiInterface
}

View File

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

View File

@ -0,0 +1,25 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import (
"github.com/mattermost/platform/model"
)
type SamlInterface interface {
ConfigureSP() *model.AppError
BuildRequest(relayState string) (*model.SamlAuthRequest, *model.AppError)
DoLogin(encodedXML string, relayState map[string]string) (*model.User, *model.AppError)
GetMetadata() (string, *model.AppError)
}
var theSamlInterface SamlInterface
func RegisterSamlInterface(newInterface SamlInterface) {
theSamlInterface = newInterface
}
func GetSamlInterface() SamlInterface {
return theSamlInterface
}

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

@ -7,7 +7,9 @@ import (
"bytes"
"fmt"
l4g "github.com/alecthomas/log4go"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"strconv"
@ -18,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"
@ -30,6 +33,7 @@ const (
HEADER_REQUESTED_WITH_XML = "XMLHttpRequest"
STATUS = "status"
STATUS_OK = "OK"
STATUS_FAIL = "FAIL"
API_URL_SUFFIX_V1 = "/api/v1"
API_URL_SUFFIX_V3 = "/api/v3"
@ -106,6 +110,10 @@ func (c *Client) GetChannelNameRoute(channelName string) string {
return fmt.Sprintf("/teams/%v/channels/name/%v", c.GetTeamId(), channelName)
}
func (c *Client) GetEmojiRoute() string {
return "/emoji"
}
func (c *Client) GetGeneralRoute() string {
return "/general"
}
@ -185,6 +193,17 @@ func (c *Client) Must(result *Result, err *AppError) *Result {
return result
}
// MustGeneric is a convenience function used for testing.
func (c *Client) MustGeneric(result interface{}, err *AppError) interface{} {
if err != nil {
l4g.Close()
time.Sleep(time.Second)
panic(err)
}
return result
}
// CheckStatusOK is a convenience function for checking the return of Web Service
// call that return the a map of status=OK.
func (c *Client) CheckStatusOK(r *http.Response) bool {
@ -259,6 +278,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
@ -272,6 +294,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
@ -282,6 +306,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
@ -292,6 +318,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
@ -302,6 +329,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
@ -312,6 +341,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
@ -328,10 +359,18 @@ func (c *Client) FindTeamByName(name string) (*Result, *AppError) {
}
}
func (c *Client) AddUserToTeam(userId string) (*Result, *AppError) {
// Adds a user directly to the team without sending an invite.
// The teamId and userId are required. You must be a valid member of the team and/or
// have the correct role to add new users to the team. Returns a map of user_id=userId
// if successful, otherwise returns an AppError.
func (c *Client) AddUserToTeam(teamId string, userId string) (*Result, *AppError) {
if len(teamId) == 0 {
teamId = c.GetTeamId()
}
data := make(map[string]string)
data["user_id"] = userId
if r, err := c.DoApiPost(c.GetTeamRoute()+"/add_user_to_team", MapToJson(data)); err != nil {
if r, err := c.DoApiPost(fmt.Sprintf("/teams/%v", teamId)+"/add_user_to_team", MapToJson(data)); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -340,6 +379,8 @@ func (c *Client) AddUserToTeam(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
@ -354,6 +395,26 @@ func (c *Client) AddUserToTeamFromInvite(hash, dataToHash, inviteId string) (*Re
}
}
// Removes a user directly from the team.
// The teamId and userId are required. You must be a valid member of the team and/or
// have the correct role to remove a user from the team. Returns a map of user_id=userId
// if successful, otherwise returns an AppError.
func (c *Client) RemoveUserFromTeam(teamId string, userId string) (*Result, *AppError) {
if len(teamId) == 0 {
teamId = c.GetTeamId()
}
data := make(map[string]string)
data["user_id"] = userId
if r, err := c.DoApiPost(fmt.Sprintf("/teams/%v", teamId)+"/remove_user_from_team", 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
}
}
func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetTeamRoute()+"/invite_members", invites.ToJson()); err != nil {
return nil, err
@ -364,6 +425,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
@ -374,6 +438,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
@ -384,6 +451,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)
@ -407,6 +476,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
@ -417,6 +487,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
@ -427,6 +498,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
@ -437,6 +510,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
@ -447,6 +522,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
@ -457,6 +534,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
@ -464,6 +542,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
@ -471,6 +551,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
@ -479,6 +560,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
@ -505,6 +589,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
@ -519,6 +604,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
@ -532,6 +620,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
@ -542,6 +632,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
@ -716,6 +809,15 @@ 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
}
}
func (c *Client) GetAllAudits() (*Result, *AppError) {
if r, err := c.DoApiGet("/admin/audits", "", ""); err != nil {
return nil, err
@ -843,6 +945,20 @@ func (c *Client) GetSystemAnalytics(name string) (*Result, *AppError) {
}
}
// Initiate immediate synchronization of LDAP users.
// The synchronization will be performed asynchronously and this function will
// always return OK unless you don't have permissions.
// You must be the system administrator to use this function.
func (c *Client) LdapSyncNow() (*Result, *AppError) {
if r, err := c.DoApiPost("/admin/ldap_sync_now", ""); 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) CreateChannel(channel *Channel) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/create", channel.ToJson()); err != nil {
return nil, err
@ -949,6 +1065,7 @@ func (c *Client) JoinChannel(id string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/join", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), nil}, nil
}
@ -958,6 +1075,7 @@ func (c *Client) JoinChannelByName(name string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelNameRoute(name)+"/join", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), nil}, nil
}
@ -967,6 +1085,7 @@ func (c *Client) LeaveChannel(id string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/leave", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), nil}, nil
}
@ -976,6 +1095,7 @@ func (c *Client) DeleteChannel(id string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/delete", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), nil}, nil
}
@ -987,6 +1107,7 @@ func (c *Client) AddChannelMember(id, user_id string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/add", 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), nil}, nil
}
@ -998,6 +1119,7 @@ func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/remove", 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), nil}, nil
}
@ -1007,6 +1129,7 @@ func (c *Client) UpdateLastViewedAt(channelId string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/update_last_viewed_at", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), nil}, nil
}
@ -1115,6 +1238,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)
}
@ -1302,8 +1437,9 @@ 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)
@ -1332,6 +1468,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
@ -1342,6 +1480,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
@ -1352,8 +1493,47 @@ 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
}
}
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)
@ -1376,6 +1556,7 @@ func (c *Client) PostToWebhook(id, payload string) (*Result, *AppError) {
if r, err := c.DoPost("/hooks/"+id, payload, "application/x-www-form-urlencoded"); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), nil}, nil
}
@ -1417,6 +1598,7 @@ func (c *Client) SetPreferences(preferences *Preferences) (*Result, *AppError) {
if r, err := c.DoApiPost("/preferences/save", preferences.ToJson()); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), preferences}, nil
}
@ -1441,6 +1623,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
@ -1509,3 +1701,118 @@ func (c *Client) GetInitialLoad() (*Result, *AppError) {
r.Header.Get(HEADER_ETAG_SERVER), InitialLoadFromJson(r.Body)}, nil
}
}
// ListEmoji returns a list of all user-created emoji for the server.
func (c *Client) ListEmoji() ([]*Emoji, *AppError) {
if r, err := c.DoApiGet(c.GetEmojiRoute()+"/list", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
c.fillInExtraProperties(r)
return EmojiListFromJson(r.Body), nil
}
}
// CreateEmoji will save an emoji to the server if the current user has permission
// to do so. If successful, the provided emoji will be returned with its Id field
// filled in. Otherwise, an error will be returned.
func (c *Client) CreateEmoji(emoji *Emoji, image []byte, filename string) (*Emoji, *AppError) {
c.clearExtraProperties()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
if part, err := writer.CreateFormFile("image", filename); err != nil {
return nil, NewLocAppError("CreateEmoji", "model.client.create_emoji.image.app_error", nil, err.Error())
} else if _, err = io.Copy(part, bytes.NewBuffer(image)); err != nil {
return nil, NewLocAppError("CreateEmoji", "model.client.create_emoji.image.app_error", nil, err.Error())
}
if err := writer.WriteField("emoji", emoji.ToJson()); err != nil {
return nil, NewLocAppError("CreateEmoji", "model.client.create_emoji.emoji.app_error", nil, err.Error())
}
if err := writer.Close(); err != nil {
return nil, NewLocAppError("CreateEmoji", "model.client.create_emoji.writer.app_error", nil, err.Error())
}
rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetEmojiRoute()+"/create", body)
rq.Header.Set("Content-Type", writer.FormDataContentType())
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
}
if r, err := c.HttpClient.Do(rq); err != nil {
return nil, NewLocAppError("CreateEmoji", "model.client.connecting.app_error", nil, err.Error())
} else if r.StatusCode >= 300 {
return nil, AppErrorFromJson(r.Body)
} else {
defer closeBody(r)
c.fillInExtraProperties(r)
return EmojiFromJson(r.Body), nil
}
}
// DeleteEmoji will delete an emoji from the server if the current user has permission
// to do so. If successful, it will return status=ok. Otherwise, an error will be returned.
func (c *Client) DeleteEmoji(id string) (bool, *AppError) {
data := map[string]string{"id": id}
if r, err := c.DoApiPost(c.GetEmojiRoute()+"/delete", MapToJson(data)); err != nil {
return false, err
} else {
c.fillInExtraProperties(r)
return c.CheckStatusOK(r), nil
}
}
// GetCustomEmojiImageUrl returns the API route that can be used to get the image used by
// the given emoji.
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,7 +6,7 @@ package model
import (
"encoding/json"
"io"
"strings"
"net/url"
)
const (
@ -20,8 +20,12 @@ const (
DATABASE_DRIVER_MYSQL = "mysql"
DATABASE_DRIVER_POSTGRES = "postgres"
SERVICE_GITLAB = "gitlab"
SERVICE_GOOGLE = "google"
PASSWORD_MAXIMUM_LENGTH = 64
PASSWORD_MINIMUM_LENGTH = 5
SERVICE_GITLAB = "gitlab"
SERVICE_GOOGLE = "google"
SERVICE_OFFICE365 = "office365"
WEBSERVER_MODE_REGULAR = "regular"
WEBSERVER_MODE_GZIP = "gzip"
@ -33,19 +37,21 @@ const (
DIRECT_MESSAGE_ANY = "any"
DIRECT_MESSAGE_TEAM = "team"
PERMISSIONS_ALL = "all"
PERMISSIONS_TEAM_ADMIN = "team_admin"
PERMISSIONS_SYSTEM_ADMIN = "system_admin"
FAKE_SETTING = "********************************"
RESTRICT_EMOJI_CREATION_ALL = "all"
RESTRICT_EMOJI_CREATION_ADMIN = "admin"
RESTRICT_EMOJI_CREATION_SYSTEM_ADMIN = "system_admin"
SITENAME_MAX_LENGTH = 30
)
// should match the values in webapp/i18n/i18n.jsx
var LOCALES = []string{
"en",
"es",
"fr",
"ja",
"pt-BR",
}
type ServiceSettings struct {
SiteURL *string
ListenAddress string
MaximumLoginAttempts int
SegmentDeveloperKey string
@ -70,6 +76,14 @@ type ServiceSettings struct {
WebsocketSecurePort *int
WebsocketPort *int
WebserverMode *string
EnableCustomEmoji *bool
RestrictCustomEmojiCreation *string
}
type ClusterSettings struct {
Enable *bool
InterNodeListenAddress *string
InterNodeUrls []string
}
type SSOSettings struct {
@ -93,12 +107,21 @@ type SqlSettings struct {
}
type LogSettings struct {
EnableConsole bool
ConsoleLevel string
EnableFile bool
FileLevel string
FileFormat string
FileLocation string
EnableConsole bool
ConsoleLevel string
EnableFile bool
FileLevel string
FileFormat string
FileLocation string
EnableWebhookDebugging bool
}
type PasswordSettings struct {
MinimumLength *int
Lowercase *bool
Number *bool
Uppercase *bool
Symbol *bool
}
type FileSettings struct {
@ -132,6 +155,7 @@ type EmailSettings struct {
RequireEmailVerification bool
FeedbackName string
FeedbackEmail string
FeedbackOrganization *string
SMTPUsername string
SMTPPassword string
SMTPServer string
@ -167,16 +191,21 @@ type SupportSettings struct {
}
type TeamSettings struct {
SiteName string
MaxUsersPerTeam int
EnableTeamCreation bool
EnableUserCreation bool
EnableOpenServer *bool
RestrictCreationToDomains string
RestrictTeamNames *bool
EnableCustomBrand *bool
CustomBrandText *string
RestrictDirectMessage *string
SiteName string
MaxUsersPerTeam int
EnableTeamCreation bool
EnableUserCreation bool
EnableOpenServer *bool
RestrictCreationToDomains string
RestrictTeamNames *bool
EnableCustomBrand *bool
CustomBrandText *string
CustomDescriptionText *string
RestrictDirectMessage *string
RestrictTeamInvite *string
RestrictPublicChannelManagement *string
RestrictPrivateChannelManagement *string
UserStatusAwayTimeout *int64
}
type LdapSettings struct {
@ -206,6 +235,7 @@ type LdapSettings struct {
// Advanced
SkipCertificateVerification *bool
QueryTimeout *int
MaxPageSize *int
// Customization
LoginFieldName *string
@ -223,11 +253,43 @@ type LocalizationSettings struct {
AvailableLocales *string
}
type SamlSettings struct {
// Basic
Enable *bool
Verify *bool
Encrypt *bool
IdpUrl *string
IdpDescriptorUrl *string
AssertionConsumerServiceURL *string
IdpCertificateFile *string
PublicCertificateFile *string
PrivateKeyFile *string
// User Mapping
FirstNameAttribute *string
LastNameAttribute *string
EmailAttribute *string
UsernameAttribute *string
NicknameAttribute *string
LocaleAttribute *string
LoginButtonText *string
}
type NativeAppSettings struct {
AppDownloadLink *string
AndroidAppDownloadLink *string
IosAppDownloadLink *string
}
type Config struct {
ServiceSettings ServiceSettings
TeamSettings TeamSettings
SqlSettings SqlSettings
LogSettings LogSettings
PasswordSettings PasswordSettings
FileSettings FileSettings
EmailSettings EmailSettings
RateLimitSettings RateLimitSettings
@ -235,9 +297,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 {
@ -255,6 +321,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
@ -304,6 +372,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
@ -324,6 +397,31 @@ func (o *Config) SetDefaults() {
*o.ServiceSettings.EnableMultifactorAuthentication = false
}
if o.PasswordSettings.MinimumLength == nil {
o.PasswordSettings.MinimumLength = new(int)
*o.PasswordSettings.MinimumLength = PASSWORD_MINIMUM_LENGTH
}
if o.PasswordSettings.Lowercase == nil {
o.PasswordSettings.Lowercase = new(bool)
*o.PasswordSettings.Lowercase = false
}
if o.PasswordSettings.Number == nil {
o.PasswordSettings.Number = new(bool)
*o.PasswordSettings.Number = false
}
if o.PasswordSettings.Uppercase == nil {
o.PasswordSettings.Uppercase = new(bool)
*o.PasswordSettings.Uppercase = false
}
if o.PasswordSettings.Symbol == nil {
o.PasswordSettings.Symbol = new(bool)
*o.PasswordSettings.Symbol = false
}
if o.TeamSettings.RestrictTeamNames == nil {
o.TeamSettings.RestrictTeamNames = new(bool)
*o.TeamSettings.RestrictTeamNames = true
@ -339,6 +437,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
@ -349,6 +452,26 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.RestrictDirectMessage = DIRECT_MESSAGE_ANY
}
if o.TeamSettings.RestrictTeamInvite == nil {
o.TeamSettings.RestrictTeamInvite = new(string)
*o.TeamSettings.RestrictTeamInvite = PERMISSIONS_ALL
}
if o.TeamSettings.RestrictPublicChannelManagement == nil {
o.TeamSettings.RestrictPublicChannelManagement = new(string)
*o.TeamSettings.RestrictPublicChannelManagement = PERMISSIONS_ALL
}
if o.TeamSettings.RestrictPrivateChannelManagement == nil {
o.TeamSettings.RestrictPrivateChannelManagement = new(string)
*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)
@ -379,13 +502,18 @@ func (o *Config) SetDefaults() {
*o.EmailSettings.PushNotificationContents = GENERIC_NOTIFICATION
}
if o.EmailSettings.FeedbackOrganization == nil {
o.EmailSettings.FeedbackOrganization = new(string)
*o.EmailSettings.FeedbackOrganization = ""
}
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) {
@ -394,7 +522,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) {
@ -403,7 +531,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) {
@ -412,7 +540,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) {
@ -421,7 +549,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 {
@ -484,6 +612,11 @@ func (o *Config) SetDefaults() {
*o.LdapSettings.EmailAttribute = ""
}
if o.LdapSettings.UsernameAttribute == nil {
o.LdapSettings.UsernameAttribute = new(string)
*o.LdapSettings.UsernameAttribute = ""
}
if o.LdapSettings.NicknameAttribute == nil {
o.LdapSettings.NicknameAttribute = new(string)
*o.LdapSettings.NicknameAttribute = ""
@ -509,6 +642,11 @@ func (o *Config) SetDefaults() {
*o.LdapSettings.QueryTimeout = 60
}
if o.LdapSettings.MaxPageSize == nil {
o.LdapSettings.MaxPageSize = new(int)
*o.LdapSettings.MaxPageSize = 0
}
if o.LdapSettings.LoginFieldName == nil {
o.LdapSettings.LoginFieldName = new(string)
*o.LdapSettings.LoginFieldName = ""
@ -561,7 +699,33 @@ func (o *Config) SetDefaults() {
if o.ServiceSettings.WebserverMode == nil {
o.ServiceSettings.WebserverMode = new(string)
*o.ServiceSettings.WebserverMode = "regular"
*o.ServiceSettings.WebserverMode = "gzip"
} else if *o.ServiceSettings.WebserverMode == "regular" {
*o.ServiceSettings.WebserverMode = "gzip"
}
if o.ServiceSettings.EnableCustomEmoji == nil {
o.ServiceSettings.EnableCustomEmoji = new(bool)
*o.ServiceSettings.EnableCustomEmoji = true
}
if o.ServiceSettings.RestrictCustomEmojiCreation == nil {
o.ServiceSettings.RestrictCustomEmojiCreation = new(string)
*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 {
@ -591,7 +755,102 @@ func (o *Config) SetDefaults() {
if o.LocalizationSettings.AvailableLocales == nil {
o.LocalizationSettings.AvailableLocales = new(string)
*o.LocalizationSettings.AvailableLocales = strings.Join(LOCALES, ",")
*o.LocalizationSettings.AvailableLocales = ""
}
if o.SamlSettings.Enable == nil {
o.SamlSettings.Enable = new(bool)
*o.SamlSettings.Enable = false
}
if o.SamlSettings.Verify == nil {
o.SamlSettings.Verify = new(bool)
*o.SamlSettings.Verify = false
}
if o.SamlSettings.Encrypt == nil {
o.SamlSettings.Encrypt = new(bool)
*o.SamlSettings.Encrypt = false
}
if o.SamlSettings.IdpUrl == nil {
o.SamlSettings.IdpUrl = new(string)
*o.SamlSettings.IdpUrl = ""
}
if o.SamlSettings.IdpDescriptorUrl == nil {
o.SamlSettings.IdpDescriptorUrl = new(string)
*o.SamlSettings.IdpDescriptorUrl = ""
}
if o.SamlSettings.IdpCertificateFile == nil {
o.SamlSettings.IdpCertificateFile = new(string)
*o.SamlSettings.IdpCertificateFile = ""
}
if o.SamlSettings.PublicCertificateFile == nil {
o.SamlSettings.PublicCertificateFile = new(string)
*o.SamlSettings.PublicCertificateFile = ""
}
if o.SamlSettings.PrivateKeyFile == nil {
o.SamlSettings.PrivateKeyFile = new(string)
*o.SamlSettings.PrivateKeyFile = ""
}
if o.SamlSettings.AssertionConsumerServiceURL == nil {
o.SamlSettings.AssertionConsumerServiceURL = new(string)
*o.SamlSettings.AssertionConsumerServiceURL = ""
}
if o.SamlSettings.LoginButtonText == nil || *o.SamlSettings.LoginButtonText == "" {
o.SamlSettings.LoginButtonText = new(string)
*o.SamlSettings.LoginButtonText = USER_AUTH_SERVICE_SAML_TEXT
}
if o.SamlSettings.FirstNameAttribute == nil {
o.SamlSettings.FirstNameAttribute = new(string)
*o.SamlSettings.FirstNameAttribute = ""
}
if o.SamlSettings.LastNameAttribute == nil {
o.SamlSettings.LastNameAttribute = new(string)
*o.SamlSettings.LastNameAttribute = ""
}
if o.SamlSettings.EmailAttribute == nil {
o.SamlSettings.EmailAttribute = new(string)
*o.SamlSettings.EmailAttribute = ""
}
if o.SamlSettings.UsernameAttribute == nil {
o.SamlSettings.UsernameAttribute = new(string)
*o.SamlSettings.UsernameAttribute = ""
}
if o.SamlSettings.NicknameAttribute == nil {
o.SamlSettings.NicknameAttribute = new(string)
*o.SamlSettings.NicknameAttribute = ""
}
if o.SamlSettings.LocaleAttribute == nil {
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/"
}
}
@ -601,6 +860,12 @@ 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, "")
}
@ -697,6 +962,98 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_sync_interval.app_error", nil, "")
}
if *o.LdapSettings.MaxPageSize < 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_max_page_size.app_error", nil, "")
}
if *o.LdapSettings.Enable {
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.FirstNameAttribute == "" {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_firstname", nil, "")
}
if *o.LdapSettings.LastNameAttribute == "" {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_lastname", 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 || !IsValidHttpUrl(*o.SamlSettings.IdpUrl) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_idp_url.app_error", nil, "")
}
if len(*o.SamlSettings.IdpDescriptorUrl) == 0 || !IsValidHttpUrl(*o.SamlSettings.IdpDescriptorUrl) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_idp_descriptor_url.app_error", nil, "")
}
if len(*o.SamlSettings.IdpCertificateFile) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_idp_cert.app_error", nil, "")
}
if len(*o.SamlSettings.EmailAttribute) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_email_attribute.app_error", nil, "")
}
if len(*o.SamlSettings.UsernameAttribute) == 0 {
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, "")
}
}
if *o.SamlSettings.Encrypt {
if len(*o.SamlSettings.PrivateKeyFile) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_private_key.app_error", nil, "")
}
if len(*o.SamlSettings.PublicCertificateFile) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_public_cert.app_error", nil, "")
}
}
if len(*o.SamlSettings.EmailAttribute) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_email_attribute.app_error", nil, "")
}
}
if *o.PasswordSettings.MinimumLength < PASSWORD_MINIMUM_LENGTH || *o.PasswordSettings.MinimumLength > PASSWORD_MAXIMUM_LENGTH {
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
}

95
vendor/github.com/mattermost/platform/model/emoji.go generated vendored Normal file
View File

@ -0,0 +1,95 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type Emoji struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
CreatorId string `json:"creator_id"`
Name string `json:"name"`
}
func (emoji *Emoji) IsValid() *AppError {
if len(emoji.Id) != 26 {
return NewLocAppError("Emoji.IsValid", "model.emoji.id.app_error", nil, "")
}
if emoji.CreateAt == 0 {
return NewLocAppError("Emoji.IsValid", "model.emoji.create_at.app_error", nil, "id="+emoji.Id)
}
if emoji.UpdateAt == 0 {
return NewLocAppError("Emoji.IsValid", "model.emoji.update_at.app_error", nil, "id="+emoji.Id)
}
if len(emoji.CreatorId) != 26 {
return NewLocAppError("Emoji.IsValid", "model.emoji.user_id.app_error", nil, "")
}
if len(emoji.Name) == 0 || len(emoji.Name) > 64 {
return NewLocAppError("Emoji.IsValid", "model.emoji.name.app_error", nil, "")
}
return nil
}
func (emoji *Emoji) PreSave() {
if emoji.Id == "" {
emoji.Id = NewId()
}
emoji.CreateAt = GetMillis()
emoji.UpdateAt = emoji.CreateAt
}
func (emoji *Emoji) PreUpdate() {
emoji.UpdateAt = GetMillis()
}
func (emoji *Emoji) ToJson() string {
b, err := json.Marshal(emoji)
if err != nil {
return ""
} else {
return string(b)
}
}
func EmojiFromJson(data io.Reader) *Emoji {
decoder := json.NewDecoder(data)
var emoji Emoji
err := decoder.Decode(&emoji)
if err == nil {
return &emoji
} else {
return nil
}
}
func EmojiListToJson(emojiList []*Emoji) string {
b, err := json.Marshal(emojiList)
if err != nil {
return ""
} else {
return string(b)
}
}
func EmojiListFromJson(data io.Reader) []*Emoji {
decoder := json.NewDecoder(data)
var emojiList []*Emoji
err := decoder.Decode(&emojiList)
if err == nil {
return emojiList
} else {
return nil
}
}

View File

@ -28,8 +28,11 @@ func removeTaskByName(name string) {
delete(tasks, name)
}
func getTaskByName(name string) *ScheduledTask {
return tasks[name]
func GetTaskByName(name string) *ScheduledTask {
if task, ok := tasks[name]; ok {
return task
}
return nil
}
func GetAllTasks() *map[string]*ScheduledTask {
@ -81,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

@ -5,4 +5,5 @@ package model
const (
USER_AUTH_SERVICE_LDAP = "ldap"
LDAP_SYNC_TASK_NAME = "LDAP Syncronization"
)

View File

@ -32,14 +32,18 @@ type Customer struct {
}
type Features struct {
Users *int `json:"users"`
LDAP *bool `json:"ldap"`
MFA *bool `json:"mfa"`
GoogleSSO *bool `json:"google_sso"`
Compliance *bool `json:"compliance"`
CustomBrand *bool `json:"custom_brand"`
MHPNS *bool `json:"mhpns"`
FutureFeatures *bool `json:"future_features"`
Users *int `json:"users"`
LDAP *bool `json:"ldap"`
MFA *bool `json:"mfa"`
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"`
PasswordRequirements *bool `json:"password_requirements"`
FutureFeatures *bool `json:"future_features"`
}
func (f *Features) SetDefaults() {
@ -63,9 +67,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 {
@ -73,6 +82,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
@ -82,6 +96,16 @@ func (f *Features) SetDefaults() {
f.MHPNS = new(bool)
*f.MHPNS = *f.FutureFeatures
}
if f.SAML == nil {
f.SAML = new(bool)
*f.SAML = *f.FutureFeatures
}
if f.PasswordRequirements == nil {
f.PasswordRequirements = new(bool)
*f.PasswordRequirements = *f.FutureFeatures
}
}
func (l *License) IsExpired() bool {

View File

@ -1,60 +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_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

@ -162,9 +162,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,10 +15,17 @@ 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_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"
)
@ -57,13 +66,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)
}
}
}

18
vendor/github.com/mattermost/platform/model/saml.go generated vendored Normal file
View File

@ -0,0 +1,18 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
const (
USER_AUTH_SERVICE_SAML = "saml"
USER_AUTH_SERVICE_SAML_TEXT = "With SAML"
SAML_IDP_CERTIFICATE = 1
SAML_PRIVATE_KEY = 2
SAML_PUBLIC_CERT = 3
)
type SamlAuthRequest struct {
Base64AuthRequest string
URL string
RelayState string
}

View File

@ -83,7 +83,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) {

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

@ -0,0 +1,42 @@
// 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
)
type Status struct {
UserId string `json:"user_id"`
Status string `json:"status"`
LastActivityAt int64 `json:"last_activity_at"`
}
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

@ -14,9 +14,10 @@ const (
)
type TeamMember struct {
TeamId string `json:"team_id"`
UserId string `json:"user_id"`
Roles string `json:"roles"`
TeamId string `json:"team_id"`
UserId string `json:"user_id"`
Roles string `json:"roles"`
DeleteAt int64 `json:"delete_at"`
}
func (o *TeamMember) ToJson() string {

View File

@ -16,18 +16,12 @@ 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"
DEFAULT_LOCALE = "en"
USER_AUTH_SERVICE_EMAIL = "email"
USER_AUTH_SERVICE_USERNAME = "username"
MIN_PASSWORD_LENGTH = 5
)
type User struct {
@ -45,12 +39,9 @@ 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"`
@ -95,10 +86,6 @@ func (u *User) IsValid() *AppError {
return NewLocAppError("User.IsValid", "model.user.is_valid.last_name.app_error", nil, "user_id="+u.Id)
}
if len(u.Password) > 128 {
return NewLocAppError("User.IsValid", "model.user.is_valid.pwd.app_error", nil, "user_id="+u.Id)
}
if u.AuthData != nil && len(*u.AuthData) > 128 {
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data.app_error", nil, "user_id="+u.Id)
}
@ -111,10 +98,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
}
@ -184,21 +167,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() {
@ -208,7 +176,6 @@ func (u *User) SetDefaultNotifications() {
u.NotifyProps["desktop"] = USER_NOTIFY_ALL
u.NotifyProps["desktop_sound"] = "true"
u.NotifyProps["mention_keys"] = u.Username + ",@" + u.Username
u.NotifyProps["all"] = "true"
u.NotifyProps["channel"] = "true"
if u.FirstName == "" {
@ -244,16 +211,8 @@ func (u *User) ToJson() string {
}
// Generate a valid strong etag so the browser can cache the results
func (u *User) Etag() string {
return Etag(u.Id, u.UpdateAt)
}
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
func (u *User) Etag(showFullName, showEmail bool) string {
return Etag(u.Id, u.UpdateAt, showFullName, showEmail)
}
// Remove any private data from the user object
@ -284,11 +243,9 @@ 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
@ -363,13 +320,13 @@ func isValidRole(role string) bool {
return false
}
// Make sure you acually want to use this function. In context.go there are functions to check permssions
// Make sure you acually want to use this function. In context.go there are functions to check permissions
// This function should not be used to check permissions.
func (u *User) IsInRole(inRole string) bool {
return IsInRole(u.Roles, inRole)
}
// Make sure you acually want to use this function. In context.go there are functions to check permssions
// Make sure you acually want to use this function. In context.go there are functions to check permissions
// This function should not be used to check permissions.
func IsInRole(userRoles string, inRole string) bool {
roles := strings.Split(userRoles, " ")
@ -398,17 +355,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)
@ -467,6 +413,7 @@ var validUsernameChars = regexp.MustCompile(`^[a-z0-9\.\-_]+$`)
var restrictedUsernames = []string{
"all",
"channel",
"matterbot",
}
func IsValidUsername(s string) bool {

View File

@ -20,6 +20,13 @@ import (
"github.com/pborman/uuid"
)
const (
LOWERCASE_LETTERS = "abcdefghijklmnopqrstuvwxyz"
UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
NUMBERS = "0123456789"
SYMBOLS = " !\"\\#$%&'()*+,-./:;<=>?@[]^_`|~"
)
type StringInterface map[string]interface{}
type StringMap map[string]string
type StringArray []string
@ -27,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.3.0",
"3.2.0",
"3.1.0",
"3.0.0",
"2.2.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,114 @@
// 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_REMOVED = "user_removed"
WEBSOCKET_EVENT_PREFERENCE_CHANGED = "preference_changed"
WEBSOCKET_EVENT_EPHEMERAL_MESSAGE = "ephemeral_message"
WEBSOCKET_EVENT_STATUS_CHANGE = "status_change"
)
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
}

201
vendor/github.com/sromku/go-gitter/LICENSE generated vendored Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

70
vendor/github.com/sromku/go-gitter/faye.go generated vendored Normal file
View File

@ -0,0 +1,70 @@
package gitter
import (
"encoding/json"
"fmt"
"github.com/mrexodia/wray"
)
type Faye struct {
endpoint string
Event chan Event
client *wray.FayeClient
gitter *Gitter
}
func (gitter *Gitter) Faye(roomID string) *Faye {
wray.RegisterTransports([]wray.Transport{
&wray.HttpTransport{
SendHook: func(data map[string]interface{}) {
if channel, ok := data["channel"]; ok && channel == "/meta/handshake" {
data["ext"] = map[string]interface{}{"token": gitter.config.token}
}
},
},
})
return &Faye{
endpoint: "/api/v1/rooms/" + roomID + "/chatMessages",
Event: make(chan Event),
client: wray.NewFayeClient(fayeBaseURL),
gitter: gitter,
}
}
func (faye *Faye) Listen() {
defer faye.destroy()
faye.client.Subscribe(faye.endpoint, false, func(message wray.Message) {
dataBytes, err := json.Marshal(message.Data["model"])
if err != nil {
fmt.Printf("JSON Marshal error: %v\n", err)
return
}
var gitterMessage Message
err = json.Unmarshal(dataBytes, &gitterMessage)
if err != nil {
fmt.Printf("JSON Unmarshal error: %v\n", err)
return
}
faye.Event <- Event{
Data: &MessageReceived{
Message: gitterMessage,
},
}
})
//TODO: this might be needed in the future
/*go func() {
for {
faye.client.Publish("/api/v1/ping2", map[string]interface{}{"reason": "ping"})
time.Sleep(60 * time.Second)
}
}()*/
faye.client.Listen()
}
func (faye *Faye) destroy() {
close(faye.Event)
}

367
vendor/github.com/sromku/go-gitter/gitter.go generated vendored Normal file
View File

@ -0,0 +1,367 @@
// Package gitter is a Go client library for the Gitter API.
//
// Author: sromku
package gitter
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"time"
"github.com/mreiferson/go-httpclient"
)
var (
apiBaseURL = "https://api.gitter.im/v1/"
streamBaseURL = "https://stream.gitter.im/v1/"
fayeBaseURL = "https://ws.gitter.im/faye"
)
type Gitter struct {
config struct {
apiBaseURL string
streamBaseURL string
token string
client *http.Client
}
debug bool
logWriter io.Writer
}
// New initializes the Gitter API client
//
// For example:
// api := gitter.New("YOUR_ACCESS_TOKEN")
func New(token string) *Gitter {
transport := &httpclient.Transport{
ConnectTimeout: 5 * time.Second,
ReadWriteTimeout: 40 * time.Second,
}
defer transport.Close()
s := &Gitter{}
s.config.apiBaseURL = apiBaseURL
s.config.streamBaseURL = streamBaseURL
s.config.token = token
s.config.client = &http.Client{
Transport: transport,
}
return s
}
// SetClient sets a custom http client. Can be useful in App Engine case.
func (gitter *Gitter) SetClient(client *http.Client) {
gitter.config.client = client
}
// GetUser returns the current user
func (gitter *Gitter) GetUser() (*User, error) {
var users []User
response, err := gitter.get(gitter.config.apiBaseURL + "user")
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &users)
if err != nil {
gitter.log(err)
return nil, err
}
if len(users) > 0 {
return &users[0], nil
}
err = APIError{What: "Failed to retrieve current user"}
gitter.log(err)
return nil, err
}
// GetUserRooms returns a list of Rooms the user is part of
func (gitter *Gitter) GetUserRooms(userID string) ([]Room, error) {
var rooms []Room
response, err := gitter.get(gitter.config.apiBaseURL + "user/" + userID + "/rooms")
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &rooms)
if err != nil {
gitter.log(err)
return nil, err
}
return rooms, nil
}
// GetRooms returns a list of rooms the current user is in
func (gitter *Gitter) GetRooms() ([]Room, error) {
var rooms []Room
response, err := gitter.get(gitter.config.apiBaseURL + "rooms")
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &rooms)
if err != nil {
gitter.log(err)
return nil, err
}
return rooms, nil
}
// GetRoom returns a room with the passed id
func (gitter *Gitter) GetRoom(roomID string) (*Room, error) {
var room Room
response, err := gitter.get(gitter.config.apiBaseURL + "rooms/" + roomID)
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &room)
if err != nil {
gitter.log(err)
return nil, err
}
return &room, nil
}
// GetMessages returns a list of messages in a room.
// Pagination is optional. You can pass nil or specific pagination params.
func (gitter *Gitter) GetMessages(roomID string, params *Pagination) ([]Message, error) {
var messages []Message
url := gitter.config.apiBaseURL + "rooms/" + roomID + "/chatMessages"
if params != nil {
url += "?" + params.encode()
}
response, err := gitter.get(url)
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &messages)
if err != nil {
gitter.log(err)
return nil, err
}
return messages, nil
}
// GetMessage returns a message in a room.
func (gitter *Gitter) GetMessage(roomID, messageID string) (*Message, error) {
var message Message
response, err := gitter.get(gitter.config.apiBaseURL + "rooms/" + roomID + "/chatMessages/" + messageID)
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &message)
if err != nil {
gitter.log(err)
return nil, err
}
return &message, nil
}
// SendMessage sends a message to a room
func (gitter *Gitter) SendMessage(roomID, text string) error {
message := Message{Text: text}
body, _ := json.Marshal(message)
err := gitter.post(gitter.config.apiBaseURL+"rooms/"+roomID+"/chatMessages", body)
if err != nil {
gitter.log(err)
return err
}
return nil
}
// JoinRoom joins a room
func (gitter *Gitter) JoinRoom(uri string) (*Room, error) {
message := Room{URI: uri}
body, _ := json.Marshal(message)
err := gitter.post(apiBaseURL+"rooms", body)
if err != nil {
gitter.log(err)
return nil, err
}
rooms, err := gitter.GetRooms()
if err != nil {
gitter.log(err)
return nil, err
}
for _, room := range rooms {
if room.URI == uri {
return &room, nil
}
}
err = APIError{What: fmt.Sprintf("Joined room (%v) not found in list of rooms", uri)}
gitter.log(err)
return nil, err
}
// SetDebug traces errors if it's set to true.
func (gitter *Gitter) SetDebug(debug bool, logWriter io.Writer) {
gitter.debug = debug
gitter.logWriter = logWriter
}
// Pagination params
type Pagination struct {
// Skip n messages
Skip int
// Get messages before beforeId
BeforeID string
// Get messages after afterId
AfterID string
// Maximum number of messages to return
Limit int
// Search query
Query string
}
func (messageParams *Pagination) encode() string {
values := url.Values{}
if messageParams.AfterID != "" {
values.Add("afterId", messageParams.AfterID)
}
if messageParams.BeforeID != "" {
values.Add("beforeId", messageParams.BeforeID)
}
if messageParams.Skip > 0 {
values.Add("skip", strconv.Itoa(messageParams.Skip))
}
if messageParams.Limit > 0 {
values.Add("limit", strconv.Itoa(messageParams.Limit))
}
return values.Encode()
}
func (gitter *Gitter) getResponse(url string, stream *Stream) (*http.Response, error) {
r, err := http.NewRequest("GET", url, nil)
if err != nil {
gitter.log(err)
return nil, err
}
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Accept", "application/json")
r.Header.Set("Authorization", "Bearer "+gitter.config.token)
if stream != nil {
stream.streamConnection.request = r
}
response, err := gitter.config.client.Do(r)
if err != nil {
gitter.log(err)
return nil, err
}
return response, nil
}
func (gitter *Gitter) get(url string) ([]byte, error) {
resp, err := gitter.getResponse(url, nil)
if err != nil {
gitter.log(err)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = APIError{What: fmt.Sprintf("Status code: %v", resp.StatusCode)}
gitter.log(err)
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
gitter.log(err)
return nil, err
}
return body, nil
}
func (gitter *Gitter) post(url string, body []byte) error {
r, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
gitter.log(err)
return err
}
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Accept", "application/json")
r.Header.Set("Authorization", "Bearer "+gitter.config.token)
resp, err := gitter.config.client.Do(r)
if err != nil {
gitter.log(err)
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = APIError{What: fmt.Sprintf("Status code: %v", resp.StatusCode)}
gitter.log(err)
return err
}
return nil
}
func (gitter *Gitter) log(a interface{}) {
if gitter.debug {
log.Println(a)
if gitter.logWriter != nil {
timestamp := time.Now().Format(time.RFC3339)
msg := fmt.Sprintf("%v: %v", timestamp, a)
fmt.Fprintln(gitter.logWriter, msg)
}
}
}
// APIError holds data of errors returned from the API.
type APIError struct {
What string
}
func (e APIError) Error() string {
return fmt.Sprintf("%v", e.What)
}

142
vendor/github.com/sromku/go-gitter/model.go generated vendored Normal file
View File

@ -0,0 +1,142 @@
package gitter
import "time"
// A Room in Gitter can represent a GitHub Organization, a GitHub Repository, a Gitter Channel or a One-to-one conversation.
// In the case of the Organizations and Repositories, the access control policies are inherited from GitHub.
type Room struct {
// Room ID
ID string `json:"id"`
// Room name
Name string `json:"name"`
// Room topic. (default: GitHub repo description)
Topic string `json:"topic"`
// Room URI on Gitter
URI string `json:"uri"`
// Indicates if the room is a one-to-one chat
OneToOne bool `json:"oneToOne"`
// Count of users in the room
UserCount int `json:"userCount"`
// Number of unread messages for the current user
UnreadItems int `json:"unreadItems"`
// Number of unread mentions for the current user
Mentions int `json:"mentions"`
// Last time the current user accessed the room in ISO format
LastAccessTime time.Time `json:"lastAccessTime"`
// Indicates if the current user has disabled notifications
Lurk bool `json:"lurk"`
// Path to the room on gitter
URL string `json:"url"`
// Type of the room
// - ORG: A room that represents a GitHub Organization.
// - REPO: A room that represents a GitHub Repository.
// - ONETOONE: A one-to-one chat.
// - ORG_CHANNEL: A Gitter channel nested under a GitHub Organization.
// - REPO_CHANNEL A Gitter channel nested under a GitHub Repository.
// - USER_CHANNEL A Gitter channel nested under a GitHub User.
GithubType string `json:"githubType"`
// Tags that define the room
Tags []string `json:"tags"`
RoomMember bool `json:"roomMember"`
// Room version.
Version int `json:"v"`
}
type User struct {
// Gitter User ID
ID string `json:"id"`
// Gitter/GitHub username
Username string `json:"username"`
// Gitter/GitHub user real name
DisplayName string `json:"displayName"`
// Path to the user on Gitter
URL string `json:"url"`
// User avatar URI (small)
AvatarURLSmall string `json:"avatarUrlSmall"`
// User avatar URI (medium)
AvatarURLMedium string `json:"avatarUrlMedium"`
}
type Message struct {
// ID of the message
ID string `json:"id"`
// Original message in plain-text/markdown
Text string `json:"text"`
// HTML formatted message
HTML string `json:"html"`
// ISO formatted date of the message
Sent time.Time `json:"sent"`
// ISO formatted date of the message if edited
EditedAt time.Time `json:"editedAt"`
// User that sent the message
From User `json:"fromUser"`
// Boolean that indicates if the current user has read the message.
Unread bool `json:"unread"`
// Number of users that have read the message
ReadBy int `json:"readBy"`
// List of URLs present in the message
Urls []URL `json:"urls"`
// List of @Mentions in the message
Mentions []Mention `json:"mentions"`
// List of #Issues referenced in the message
Issues []Issue `json:"issues"`
// Version
Version int `json:"v"`
}
// Mention holds data about mentioned user in the message
type Mention struct {
// User's username
ScreenName string `json:"screenName"`
// Gitter User ID
UserID string `json:"userID"`
}
// Issue references issue in the message
type Issue struct {
// Issue number
Number string `json:"number"`
}
// URL presented in the message
type URL struct {
// URL
URL string `json:"url"`
}

220
vendor/github.com/sromku/go-gitter/stream.go generated vendored Normal file
View File

@ -0,0 +1,220 @@
package gitter
import (
"bufio"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/mreiferson/go-httpclient"
)
var defaultConnectionWaitTime time.Duration = 3000 // millis
var defaultConnectionMaxRetries = 5
// Stream initialize stream
func (gitter *Gitter) Stream(roomID string) *Stream {
return &Stream{
url: streamBaseURL + "rooms/" + roomID + "/chatMessages",
Event: make(chan Event),
gitter: gitter,
streamConnection: gitter.newStreamConnection(
defaultConnectionWaitTime,
defaultConnectionMaxRetries),
}
}
// Implemented to conform with https://developer.gitter.im/docs/streaming-api
func (gitter *Gitter) Listen(stream *Stream) {
defer stream.destroy()
var reader *bufio.Reader
var gitterMessage Message
lastKeepalive := time.Now().Unix()
// connect
stream.connect()
Loop:
for {
// if closed then stop trying
if stream.isClosed() {
stream.Event <- Event{
Data: &GitterConnectionClosed{},
}
break Loop
}
resp := stream.getResponse()
if resp.StatusCode != 200 {
gitter.log(fmt.Sprintf("Unexpected response code %v", resp.StatusCode))
continue
}
//"The JSON stream returns messages as JSON objects that are delimited by carriage return (\r)" <- Not true crap it's (\n) only
reader = bufio.NewReader(resp.Body)
line, err := reader.ReadBytes('\n')
//Check if the line only consists of whitespace
onlyWhitespace := true
for _, b := range line {
if b != ' ' && b != '\t' && b != '\r' && b != '\n' {
onlyWhitespace = false
}
}
if onlyWhitespace {
//"Parsers must be tolerant of occasional extra newline characters placed between messages."
currentKeepalive := time.Now().Unix() //interesting behavior of 100+ keepalives per seconds was observed
if currentKeepalive-lastKeepalive > 10 {
lastKeepalive = currentKeepalive
gitter.log("Keepalive was received")
}
continue
} else if stream.isClosed() {
gitter.log("Stream closed")
continue
} else if err != nil {
gitter.log("ReadBytes error: " + err.Error())
stream.connect()
continue
}
// unmarshal the streamed data
err = json.Unmarshal(line, &gitterMessage)
if err != nil {
gitter.log("JSON Unmarshal error: " + err.Error())
continue
}
// we are here, then we got the good message. pipe it forward.
stream.Event <- Event{
Data: &MessageReceived{
Message: gitterMessage,
},
}
}
gitter.log("Listening was completed")
}
// Stream holds stream data.
type Stream struct {
url string
Event chan Event
streamConnection *streamConnection
gitter *Gitter
}
func (stream *Stream) destroy() {
close(stream.Event)
}
type Event struct {
Data interface{}
}
type GitterConnectionClosed struct {
}
type MessageReceived struct {
Message Message
}
// connect and try to reconnect with
func (stream *Stream) connect() {
if stream.streamConnection.retries == stream.streamConnection.currentRetries {
stream.Close()
stream.gitter.log("Number of retries exceeded the max retries number, we are done here")
return
}
res, err := stream.gitter.getResponse(stream.url, stream)
if stream.streamConnection.canceled {
// do nothing
} else if err != nil || res.StatusCode != 200 {
stream.gitter.log("Failed to get response, trying reconnect ")
stream.gitter.log(err)
// sleep and wait
stream.streamConnection.currentRetries++
time.Sleep(time.Millisecond * stream.streamConnection.wait * time.Duration(stream.streamConnection.currentRetries))
// connect again
stream.Close()
stream.connect()
} else {
stream.gitter.log("Response was received")
stream.streamConnection.currentRetries = 0
stream.streamConnection.closed = false
stream.streamConnection.response = res
}
}
type streamConnection struct {
// connection was closed
closed bool
// canceled
canceled bool
// wait time till next try
wait time.Duration
// max tries to recover
retries int
// current streamed response
response *http.Response
// current request
request *http.Request
// current status
currentRetries int
}
// Close the stream connection and stop receiving streamed data
func (stream *Stream) Close() {
conn := stream.streamConnection
conn.closed = true
if conn.response != nil {
stream.gitter.log("Stream connection close response")
defer conn.response.Body.Close()
}
if conn.request != nil {
stream.gitter.log("Stream connection close request")
switch transport := stream.gitter.config.client.Transport.(type) {
case *httpclient.Transport:
stream.streamConnection.canceled = true
transport.CancelRequest(conn.request)
default:
}
}
conn.currentRetries = 0
}
func (stream *Stream) isClosed() bool {
return stream.streamConnection.closed
}
func (stream *Stream) getResponse() *http.Response {
return stream.streamConnection.response
}
// Optional, set stream connection properties
// wait - time in milliseconds of waiting between reconnections. Will grow exponentially.
// retries - number of reconnections retries before dropping the stream.
func (gitter *Gitter) newStreamConnection(wait time.Duration, retries int) *streamConnection {
return &streamConnection{
closed: true,
wait: wait,
retries: retries,
}
}

30
vendor/github.com/sromku/go-gitter/test_utils.go generated vendored Normal file
View File

@ -0,0 +1,30 @@
package gitter
import (
"net/http"
"net/http/httptest"
"net/url"
)
var (
mux *http.ServeMux
gitter *Gitter
server *httptest.Server
)
func setup() {
mux = http.NewServeMux()
server = httptest.NewServer(mux)
gitter = New("abc")
// Fake the API and Stream base URLs by using the test
// server URL instead.
url, _ := url.Parse(server.URL)
gitter.config.apiBaseURL = url.String() + "/"
gitter.config.streamBaseURL = url.String() + "/"
}
func teardown() {
server.Close()
}

View File

@ -152,7 +152,6 @@ func (irc *Connection) writeLoop() {
}
}
}
return
}
// Pings the server if we have not received any messages for 5 minutes
@ -439,6 +438,25 @@ func (irc *Connection) Connect(server string) error {
if len(irc.Password) > 0 {
irc.pwrite <- fmt.Sprintf("PASS %s\r\n", irc.Password)
}
resChan := make(chan *SASLResult)
if irc.UseSASL {
irc.setupSASLCallbacks(resChan)
irc.pwrite <- fmt.Sprintf("CAP LS\r\n")
// request SASL
irc.pwrite <- fmt.Sprintf("CAP REQ :sasl\r\n")
// if sasl request doesn't complete in 15 seconds, close chan and timeout
select {
case res := <-resChan:
if res.Failed {
close(resChan)
return res.Err
}
case <-time.After(time.Second * 15):
close(resChan)
return errors.New("SASL setup timed out. This shouldn't happen.")
}
}
irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick)
irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, irc.user)
return nil
@ -466,6 +484,7 @@ func IRC(nick, user string) *Connection {
KeepAlive: 4 * time.Minute,
Timeout: 1 * time.Minute,
PingFreq: 15 * time.Minute,
SASLMech: "PLAIN",
QuitMessage: "",
}
irc.setupCallbacks()

View File

@ -33,7 +33,7 @@ func (irc *Connection) RemoveCallback(eventcode string, i int) bool {
delete(irc.events[eventcode], i)
return true
}
irc.Log.Printf("Event found, but no callback found at id %s\n", i)
irc.Log.Printf("Event found, but no callback found at id %d\n", i)
return false
}
@ -64,7 +64,7 @@ func (irc *Connection) ReplaceCallback(eventcode string, i int, callback func(*E
event[i] = callback
return
}
irc.Log.Printf("Event found, but no callback found at id %s\n", i)
irc.Log.Printf("Event found, but no callback found at id %d\n", i)
}
irc.Log.Printf("Event not found. Use AddCallBack\n")
}

54
vendor/github.com/thoj/go-ircevent/irc_sasl.go generated vendored Normal file
View File

@ -0,0 +1,54 @@
package irc
import (
"encoding/base64"
"errors"
"fmt"
"strings"
)
type SASLResult struct {
Failed bool
Err error
}
func (irc *Connection) setupSASLCallbacks(result chan<- *SASLResult) {
irc.AddCallback("CAP", func(e *Event) {
if len(e.Arguments) == 3 {
if e.Arguments[1] == "LS" {
if !strings.Contains(e.Arguments[2], "sasl") {
result <- &SASLResult{true, errors.New("no SASL capability " + e.Arguments[2])}
}
}
if e.Arguments[1] == "ACK" {
if irc.SASLMech != "PLAIN" {
result <- &SASLResult{true, errors.New("only PLAIN is supported")}
}
irc.SendRaw("AUTHENTICATE " + irc.SASLMech)
}
}
})
irc.AddCallback("AUTHENTICATE", func(e *Event) {
str := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s\x00%s\x00%s", irc.SASLLogin, irc.SASLLogin, irc.SASLPassword)))
irc.SendRaw("AUTHENTICATE " + str)
})
irc.AddCallback("901", func(e *Event) {
irc.SendRaw("CAP END")
irc.SendRaw("QUIT")
result <- &SASLResult{true, errors.New(e.Arguments[1])}
})
irc.AddCallback("902", func(e *Event) {
irc.SendRaw("CAP END")
irc.SendRaw("QUIT")
result <- &SASLResult{true, errors.New(e.Arguments[1])}
})
irc.AddCallback("903", func(e *Event) {
irc.SendRaw("CAP END")
result <- &SASLResult{false, nil}
})
irc.AddCallback("904", func(e *Event) {
irc.SendRaw("CAP END")
irc.SendRaw("QUIT")
result <- &SASLResult{true, errors.New(e.Arguments[1])}
})
}

View File

@ -14,16 +14,20 @@ import (
type Connection struct {
sync.WaitGroup
Debug bool
Error chan error
Password string
UseTLS bool
TLSConfig *tls.Config
Version string
Timeout time.Duration
PingFreq time.Duration
KeepAlive time.Duration
Server string
Debug bool
Error chan error
Password string
UseTLS bool
UseSASL bool
SASLLogin string
SASLPassword string
SASLMech string
TLSConfig *tls.Config
Version string
Timeout time.Duration
PingFreq time.Duration
KeepAlive time.Duration
Server string
socket net.Conn
pwrite chan string

44
vendor/manifest vendored
View File

@ -63,8 +63,8 @@
"importpath": "github.com/mattermost/platform/einterfaces",
"repository": "https://github.com/mattermost/platform",
"vcs": "git",
"revision": "974238231b9cdbd39a825ec8e9299fbb0b51f6b8",
"branch": "release-3.1",
"revision": "20735470185e0b0ac1d15b975041ed9a2e0e43bc",
"branch": "release-3.3",
"path": "/einterfaces",
"notests": true
},
@ -72,11 +72,35 @@
"importpath": "github.com/mattermost/platform/model",
"repository": "https://github.com/mattermost/platform",
"vcs": "git",
"revision": "974238231b9cdbd39a825ec8e9299fbb0b51f6b8",
"branch": "release-3.1",
"revision": "20735470185e0b0ac1d15b975041ed9a2e0e43bc",
"branch": "release-3.3",
"path": "/model",
"notests": true
},
{
"importpath": "github.com/mattn/go-xmpp",
"repository": "https://github.com/mattn/go-xmpp",
"vcs": "git",
"revision": "e44d1877bb457f5c3991903e9934a31e55c3a2ad",
"branch": "master",
"notests": true
},
{
"importpath": "github.com/mreiferson/go-httpclient",
"repository": "https://github.com/mreiferson/go-httpclient",
"vcs": "git",
"revision": "31f0106b4474f14bc441575c19d3a5fa21aa1f6c",
"branch": "master",
"notests": true
},
{
"importpath": "github.com/mrexodia/wray",
"repository": "https://github.com/mrexodia/wray",
"vcs": "git",
"revision": "78a2c1f284ffe6ada7e2dfbd97c644e0d0f23fea",
"branch": "master",
"notests": true
},
{
"importpath": "github.com/nicksnyder/go-i18n/i18n",
"repository": "https://github.com/nicksnyder/go-i18n",
@ -110,11 +134,19 @@
"branch": "master",
"notests": true
},
{
"importpath": "github.com/sromku/go-gitter",
"repository": "https://github.com/sromku/go-gitter",
"vcs": "git",
"revision": "932bf9af423ac2da1544cb73540b3b08b1bdb181",
"branch": "master",
"notests": true
},
{
"importpath": "github.com/thoj/go-ircevent",
"repository": "https://github.com/thoj/go-ircevent",
"vcs": "",
"revision": "da78ed515c0f0833e7a92c7cc52898176198e2c1",
"vcs": "git",
"revision": "98c1902dd2097f38142384167e60206ba26f1585",
"branch": "master",
"notests": true
},