4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-06-26 21:19:22 +00:00

Compare commits

...

68 Commits

Author SHA1 Message Date
Wim
11def2edc0 Update documentation 2016-11-12 23:09:57 +01:00
Wim
65bffb8735 Release v0.8.0 2016-11-12 22:39:53 +01:00
Wim
0b4ac61435 Update documentation 2016-11-12 22:33:58 +01:00
Wim
1d5cd1d7c4 Sync with mattermost 3.5.0 2016-11-12 22:00:53 +01:00
Wim
08ebee6b4f Validate channels for samechannelgateway. Fixes #73. 2016-11-11 15:23:22 +01:00
Wim
a3dd0f1345 Add support for using avatars from discord,slack and gitter in slack 2016-11-06 00:46:32 +01:00
Wim
37873acfcd Update vendor (slack) 2016-11-06 00:07:24 +01:00
Wim
2dbe0eb557 Add support for dynamic IconURL (slack). Closes #43 2016-11-05 01:11:51 +01:00
Wim
50a0df4279 Reconnect on connection timed out (mattermost). Fixes #71 2016-11-04 23:17:49 +01:00
Wim
c3a8b7a997 Refactor modifyMessage 2016-11-04 23:03:31 +01:00
Wim
95fac548bb Reconnect on connection reset by peer (mattermost). Fixes #69 2016-11-02 20:00:00 +01:00
Wim
581847f415 Update to latest go-gitter API changes 2016-11-02 16:28:23 +01:00
Wim
1b15897135 Fix tight loop (gitter). Closes #68. 2016-11-02 16:13:22 +01:00
Wim
8e606e3cef Update documentation 2016-11-01 23:10:29 +01:00
Wim
be513622ac Add anti-flooding settings (irc). See #40 2016-11-01 22:52:28 +01:00
Wim
6f309f2108 Use names instead of id's for mentions (discord). Fixes #66 2016-10-30 22:55:34 +01:00
Wim
92d9db5a2d Override config from environment. See #50
Expects uppercase environment variables of MATTERBRIDGE_PROTOCOL_ACCOUNT_KEY="value"
e.g. you can override this config

[mattermost]
    [mattermost.work]
    Team="yourteam"
    Login="yourlogin"
    Password="yourpass"

by using
MATTERBRIDGE_MATTERMOST_WORK_TEAM="newteam"
MATTERBRIDGE_MATTERMOST_WORK_LOGIN="newlogin"
MATTERBRIDGE_MATTERMOST_WORK_PASSWORD="newpassword"
2016-10-30 22:32:29 +01:00
Wim
96620a3c2c Drop first received message on connection to avoid duplicates (slack). Fixes #55 2016-10-29 21:05:56 +02:00
Wim
5249568b8e Wait until the welcome message before connection is ok (irc). Fixes #62 2016-10-29 18:59:12 +02:00
Wim
4a336a6bba Forward channel notices too (irc) 2016-10-29 18:01:16 +02:00
Wim
60223d7f63 Update changelog 2016-10-29 17:54:37 +02:00
Wim
5131253191 Update documentation 2016-10-29 17:34:27 +02:00
Wim
035dc042a1 Fix teamid bug (mattermost) 2016-10-29 16:46:02 +02:00
Wim
dfc513530b Ignore messages from ourself (irc bridge) 2016-10-29 16:35:16 +02:00
Wim
721e0a2dcd Ignore private queries (irc bridge) 2016-10-29 16:27:07 +02:00
Wim
8452eb12da Only respond to notices from nickserv (irc bridge) 2016-10-29 16:09:58 +02:00
Wim
475bed5e19 Add support for discord channel ID. See #57 2016-10-26 01:01:36 +02:00
Wim
40a967523c Ignore empty content from discord. Fixes #58 2016-10-26 00:12:31 +02:00
Wim
d3a34af073 Add support for discord attachments. Fixes #59 2016-10-26 00:09:22 +02:00
Wim
e7107cf782 Use RTM only on API (slack). Fix #56 2016-10-25 23:29:32 +02:00
b7c918a195 Merge pull request #54 from markusgraube/patch-1
Close open strings in matterbridge.conf.sample
2016-10-24 12:55:43 +02:00
61e4c9b28c Update matterbridge.conf.sample
Close open strings
2016-10-24 12:14:51 +02:00
Wim
e93847a95e Launch every account only once. Fixes #48 2016-10-23 22:23:20 +02:00
Wim
545377742c Drop messages not from our mattermost team. Fixes #49 2016-10-23 21:16:14 +02:00
Wim
47d38192b2 Only send to channels defined in config. Fixes #53 2016-10-23 20:58:04 +02:00
Wim
ac80c47036 Update documentation in sample config about channelnames 2016-10-23 19:51:46 +02:00
Wim
1e84afbd90 Rename discord guild to server. 2016-10-23 19:51:41 +02:00
Wim
d31e641bac Add documentation about bot tag for discord 2016-10-23 18:19:18 +02:00
Wim
4380c48b4b Add irc names callback only on command. Fixes #51 2016-10-23 18:19:11 +02:00
Wim
db0e4ba8c5 Add error message about non-existing channels (slack) 2016-10-08 21:57:03 +02:00
Wim
2d6ed51d94 Bail out on samechannel gateway when a bridge fails to start 2016-10-03 09:23:55 +02:00
Wim
9ca4fe7a5e Fix matterbridge.toml.sample 2016-10-01 20:14:06 +02:00
Wim
e52b040b9c Add more irc debug on connect (when debugging enabled) 2016-10-01 20:07:59 +02:00
Wim
1accee1653 Bail out when a bridge fails to start 2016-10-01 20:07:04 +02:00
Wim
fff6f08cb6 Add samechannel gateway. See #35 2016-09-30 23:19:47 +02:00
Wim
0e527a4252 Fix slack channel join 2016-09-30 23:15:35 +02:00
Wim
f10251a1a3 Fix mattermost bridge channel join 2016-09-30 22:59:30 +02:00
Wim
0d4bad16a3 Fix sample config. Closes #38 2016-09-30 20:35:16 +02:00
Wim
8c6be434ac Remove newline splitting from outgoing mattermost messages. Should be handled by receiving bridge. 2016-09-29 23:32:12 +02:00
Wim
3ca4309e8a Split newlines for irc (#37) 2016-09-29 21:21:24 +02:00
Wim
e8a2e1af63 Fix IRC colors regexp 2016-09-22 23:48:05 +02:00
Wim
1d240140c9 Strip IRC colors. Closes #33 2016-09-21 00:33:40 +02:00
Wim
272eef544f Add support for mattermost attachments. Shows public link on bridges. Closes #32 2016-09-20 23:48:58 +02:00
Wim
fd756c5332 Use specified config file 2016-09-20 23:18:51 +02:00
Wim
dce600ad51 Fix joining slack/mattermost channels using the webhook 2016-09-20 12:20:44 +02:00
Wim
d02a737e0c Cleanup debug messages 2016-09-20 00:21:14 +02:00
Wim
98ff59c716 Cleanup discord bridge debug/info messages 2016-09-20 00:15:30 +02:00
Wim
0e96e9f9be Cleanup slack bridge debug/info messages 2016-09-20 00:13:57 +02:00
Wim
e8c7898583 Cleanup gitter bridge debug/info messages 2016-09-20 00:06:19 +02:00
Wim
11f4a6897a Cleanup xmpp bridge debug/info messages 2016-09-20 00:03:01 +02:00
Wim
002c5fd0d1 Cleanup mattermost bridge debug/info messages 2016-09-19 23:58:57 +02:00
Wim
18504ec08d Cleanup irc bridge debug/info messages 2016-09-19 23:35:47 +02:00
Wim
4737442185 Connect only once to each bridge 2016-09-19 23:05:49 +02:00
Wim
596096d6da Add the discord bridge for real 2016-09-19 21:05:13 +02:00
Wim
6af82401fc Add forgotten vendor for discord 2016-09-19 21:04:06 +02:00
Wim
a0b84beb9b Add Discord support 2016-09-19 20:53:26 +02:00
Wim
0816e96831 Update documentation 2016-09-18 21:04:28 +02:00
Wim
7baf386ede Refactor for more flexibility
* Move from gcfg to toml configuration because gcfg was too restrictive
* Implemented gateway which has support multiple in and out bridges.
* Allow for bridging the same bridges, which means eg you can now bridge between multiple mattermosts.
* Support multiple gateways
2016-09-18 19:21:15 +02:00
127 changed files with 16669 additions and 2879 deletions

115
README-0.6.md Normal file
View File

@ -0,0 +1,115 @@
# matterbridge
Simple bridge between mattermost, IRC, XMPP, Gitter and Slack
* Relays public channel messages between mattermost, IRC, XMPP, Gitter and Slack. Pick and mix.
* Supports multiple channels.
* Matterbridge can also work with private groups on your mattermost.
Look at [matterbridge.conf.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.conf.sample) for documentation and an example.
## Changelog
Since v0.6.1 support for XMPP, Gitter and Slack is added. More details in [changelog.md] (https://github.com/42wim/matterbridge/blob/master/changelog.md)
## Requirements:
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)
* [Slack] (https://www.slack.com)
## binaries
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/)
* For use with mattermost 3.3.0+ [v0.6.1](https://github.com/42wim/matterircd/releases/tag/v0.6.1)
* For use with mattermost 3.0.0-3.2.0 [v0.5.0](https://github.com/42wim/matterircd/releases/tag/v0.5.0)
## Docker
Create your matterbridge.conf file locally eg in ```/tmp/matterbridge.conf```
```
docker run -ti -v /tmp/matterbridge.conf:/matterbridge.conf 42wim/matterbridge:0.6.1
```
## Compatibility
### Mattermost
* Matterbridge v0.6.1 works with mattermost 3.3.0 and higher [3.3.0 release](https://github.com/mattermost/platform/releases/tag/v3.3.0)
* Matterbridge v0.5.0 works with mattermost 3.0.0 - 3.2.0 [3.2.0 release](https://github.com/mattermost/platform/releases/tag/v3.2.0)
#### Webhooks version
* Configured incoming/outgoing [webhooks](https://www.mattermost.org/webhooks/) on your mattermost instance.
#### Plus (API) version
* A dedicated user(bot) on your mattermost instance.
## building
Go 1.6+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH] (https://golang.org/doc/code.html#GOPATH)
```
cd $GOPATH
go get github.com/42wim/matterbridge
```
You should now have matterbridge binary in the bin directory:
```
$ ls bin/
matterbridge
```
## running
1) Copy the matterbridge.conf.sample to matterbridge.conf in the same directory as the matterbridge binary.
2) Edit matterbridge.conf with the settings for your environment. See below for more config information.
3) Now you can run matterbridge.
```
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
```
## config
### matterbridge
matterbridge looks for matterbridge.conf in current directory. (use -conf to specify another file)
Look at [matterbridge.conf.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.conf.sample) for an example.
### mattermost
#### webhooks version
You'll have to configure the incoming and outgoing webhooks.
* incoming webhooks
Go to "account settings" - integrations - "incoming webhooks".
Choose a channel at "Add a new incoming webhook", this will create a webhook URL right below.
This URL should be set in the matterbridge.conf in the [mattermost] section (see above)
* outgoing webhooks
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 (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.

View File

@ -1,38 +1,47 @@
# matterbridge
Simple bridge between mattermost, IRC, XMPP, Gitter, Slack and Discord
Simple bridge between mattermost, IRC, XMPP and Gitter
* Relays public channel messages between mattermost, IRC, XMPP and Gitter. Pick and mix.
* Relays public channel messages between multiple mattermost, IRC, XMPP, Gitter, Slack and Discord. Pick and mix.
* Supports multiple channels.
* Matterbridge -plus also works with private groups on your mattermost.
* Matterbridge can also work with private groups on your mattermost.
* Allow for bridging the same bridges, which means you can eg bridge between multiple mattermosts.
* The bridge is now a gateway which has support multiple in and out bridges. (and supports multiple gateways).
Look at [matterbridge.conf.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.conf.sample) for documentation and an example.
Look at [matterbridge.toml.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example.
Look at [matterbridge.toml.simple] (https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.simple) for a simple example.
## Changelog
Since v0.6.1 support for XMPP, Gitter and Slack is added. More details in [changelog.md] (https://github.com/42wim/matterbridge/blob/master/changelog.md)
Since v0.7.0 the configuration has changed. More details in [changelog.md] (https://github.com/42wim/matterbridge/blob/master/changelog.md)
## Requirements:
## 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)
* [Slack] (https://slack.com)
* [Discord] (https://discordapp.com)
## Docker
Create your matterbridge.toml file locally eg in ```/tmp/matterbridge.toml```
```
docker run -ti -v /tmp/matterbridge.toml:/matterbridge.toml 42wim/matterbridge
```
## binaries
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/)
* For use with mattermost 3.3.0+ [v0.6.1](https://github.com/42wim/matterircd/releases/tag/v0.6.1)
* For use with mattermost 3.0.0-3.2.0 [v0.5.0](https://github.com/42wim/matterircd/releases/tag/v0.5.0)
* For use with mattermost 3.5.0+ [v0.8.0](https://github.com/42wim/matterircd/releases/tag/v0.8.0)
* For use with mattermost 3.3.0 - 3.4.0 [v0.7.0](https://github.com/42wim/matterircd/releases/tag/v0.7.0)
## Compatibility
### Mattermost
* Matterbridge v0.6.1 works with mattermost 3.3.0 and higher [3.3.0 release](https://github.com/mattermost/platform/releases/tag/v3.3.0)
* Matterbridge v0.5.0 works with mattermost 3.0.0 - 3.2.0 [3.2.0 release](https://github.com/mattermost/platform/releases/tag/v3.2.0)
* Matterbridge v0.8.0 works with mattermost 3.5.0+ [3.5.0 release](https://github.com/mattermost/platform/releases/tag/v3.5.0)
* Matterbridge v0.7.0 works with mattermost 3.3.0 - 3.4.0 [3.4.0 release](https://github.com/mattermost/platform/releases/tag/v3.4.0)
#### Webhooks version
* Configured incoming/outgoing [webhooks](https://www.mattermost.org/webhooks/) on your mattermost instance.
#### Plus (API) version
#### API version
* A dedicated user(bot) on your mattermost instance.
@ -59,20 +68,18 @@ matterbridge
```
Usage of ./matterbridge:
-conf string
config file (default "matterbridge.conf")
config file (default "matterbridge.toml")
-debug
enable debug
-plus
running using API instead of webhooks (deprecated, set Plus flag in [general] config)
-version
show version
```
## config
### matterbridge
matterbridge looks for matterbridge.conf in current directory. (use -conf to specify another file)
matterbridge looks for matterbridge.toml in current directory. (use -conf to specify another file)
Look at [matterbridge.conf.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.conf.sample) for an example.
Look at [matterbridge.toml.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for an example.
### mattermost
#### webhooks version
@ -89,18 +96,14 @@ Choose a channel (the same as the one from incoming webhooks) and fill in the ad
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.
Please look at [matterbridge.toml.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.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.
* setting ```PrefixMessagesWithNick``` to ```true``` in ```mattermost``` section of your matterbridge.toml.
If you're running the plus version you'll need to:
* setting ```PrefixMessagesWithNick``` to ```true``` in ```mattermost``` section of your matterbridge.conf.
* setting ```PrefixMessagesWithNick``` to ```true``` in ```mattermost``` section of your matterbridge.toml.
Also look at the ```RemoteNickFormat``` setting.

View File

@ -1,151 +1,45 @@
package bridge
import (
//"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/discord"
"github.com/42wim/matterbridge/bridge/gitter"
"github.com/42wim/matterbridge/bridge/irc"
"github.com/42wim/matterbridge/bridge/mattermost"
"github.com/42wim/matterbridge/bridge/slack"
"github.com/42wim/matterbridge/bridge/xmpp"
log "github.com/Sirupsen/logrus"
"strings"
)
type Bridge struct {
*config.Config
Source string
Bridges []Bridger
Channels []map[string]string
ignoreNicks map[string][]string
}
type Bridger interface {
type Bridge interface {
Send(msg config.Message) error
Name() string
Connect() error
//Command(cmd string) string
FullOrigin() string
Origin() string
Protocol() string
JoinChannel(channel string) error
}
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 cfg.Slack.Enable {
b.Bridges = append(b.Bridges, bslack.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
m["slack"] = val.Slack
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)
m["slack"] = strings.Fields(b.Config.Slack.IgnoreNicks)
b.ignoreNicks = m
}
func (b *Bridge) getDestChannel(msg *config.Message, dest string) string {
for _, v := range b.Channels {
if v[msg.Origin] == msg.Channel {
return v[dest]
}
}
return ""
}
func (b *Bridge) handleMessage(msg config.Message, dest Bridger) {
if b.ignoreMessage(&msg) {
return
}
if dest.Name() != msg.Origin {
msg.Channel = b.getDestChannel(&msg, dest.Name())
if msg.Channel == "" {
return
}
b.modifyMessage(&msg, dest.Name())
log.Debugf("sending %#v from %s to %s", msg, msg.Origin, dest.Name())
dest.Send(msg)
}
}
func (b *Bridge) 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)
func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) Bridge {
accInfo := strings.Split(bridge.Account, ".")
protocol := accInfo[0]
name := accInfo[1]
// override config from environment
config.OverrideCfgFromEnv(cfg, protocol, name)
switch protocol {
case "mattermost":
setNickFormat(msg, b.Config.Mattermost.RemoteNickFormat)
return bmattermost.New(cfg.Mattermost[name], name, c)
case "irc":
return birc.New(cfg.IRC[name], name, c)
case "gitter":
return bgitter.New(cfg.Gitter[name], name, c)
case "slack":
setNickFormat(msg, b.Config.Slack.RemoteNickFormat)
return bslack.New(cfg.Slack[name], name, c)
case "xmpp":
return bxmpp.New(cfg.Xmpp[name], name, c)
case "discord":
return bdiscord.New(cfg.Discord[name], name, c)
}
return nil
}

View File

@ -1,9 +1,11 @@
package config
import (
"gopkg.in/gcfg.v1"
"io/ioutil"
"github.com/BurntSushi/toml"
"log"
"os"
"reflect"
"strings"
)
type Message struct {
@ -11,97 +13,129 @@ type Message struct {
Channel string
Username string
Origin string
FullOrigin string
Protocol string
Avatar string
}
type Protocol struct {
BindAddress string // mattermost, slack
IconURL string // mattermost, slack
IgnoreNicks string // all protocols
Jid string // xmpp
Login string // mattermost
Muc string // xmpp
Name string // all protocols
Nick string // all protocols
NickFormatter string // mattermost, slack
NickServNick string // IRC
NickServPassword string // IRC
NicksPerRow int // mattermost, slack
NoTLS bool // mattermost
Password string // IRC,mattermost,XMPP
PrefixMessagesWithNick bool // mattemost, slack
Protocol string //all protocols
MessageQueue int // IRC, size of message queue for flood control
MessageDelay int // IRC, time in millisecond to wait between messages
RemoteNickFormat string // all protocols
Server string // IRC,mattermost,XMPP,discord
ShowJoinPart bool // all protocols
SkipTLSVerify bool // IRC, mattermost
Team string // mattermost
Token string // gitter, slack, discord
URL string // mattermost, slack
UseAPI bool // mattermost, slack
UseSASL bool // IRC
UseTLS bool // IRC
}
type Bridge struct {
Account string
Channel string
}
type Gateway struct {
Name string
Enable bool
In []Bridge
Out []Bridge
}
type SameChannelGateway struct {
Name string
Enable bool
Channels []string
Accounts []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
}
Slack struct {
BindAddress string
Enable bool
IconURL string
IgnoreNicks string
NickFormatter string
NicksPerRow int
PrefixMessagesWithNick bool
RemoteNickFormat string
Token string
URL string
UseAPI bool
}
Xmpp struct {
IgnoreNicks string
Jid string
Password string
Server string
Muc string
Nick string
RemoteNickFormat string
Enable bool
}
Channel map[string]*struct {
IRC string
Mattermost string
Xmpp string
Gitter string
Slack string
}
General struct {
GiphyAPIKey string
Xmpp bool
Irc bool
Mattermost bool
Plus bool
}
IRC map[string]Protocol
Mattermost map[string]Protocol
Slack map[string]Protocol
Gitter map[string]Protocol
Xmpp map[string]Protocol
Discord map[string]Protocol
Gateway []Gateway
SameChannelGateway []SameChannelGateway
}
func NewConfig(cfgfile string) *Config {
var cfg Config
content, err := ioutil.ReadFile(cfgfile)
if err != nil {
if _, err := toml.DecodeFile(cfgfile, &cfg); err != nil {
log.Fatal(err)
}
err = gcfg.ReadStringInto(&cfg, string(content))
if err != nil {
log.Fatal("Failed to parse "+cfgfile+":", err)
}
return &cfg
}
func OverrideCfgFromEnv(cfg *Config, protocol string, account string) {
var protoCfg Protocol
val := reflect.ValueOf(cfg).Elem()
// loop over the Config struct
for i := 0; i < val.NumField(); i++ {
typeField := val.Type().Field(i)
// look for the protocol map (both lowercase)
if strings.ToLower(typeField.Name) == protocol {
// get the Protocol struct from the map
data := val.Field(i).MapIndex(reflect.ValueOf(account))
protoCfg = data.Interface().(Protocol)
protoStruct := reflect.ValueOf(&protoCfg).Elem()
// loop over the found protocol struct
for i := 0; i < protoStruct.NumField(); i++ {
typeField := protoStruct.Type().Field(i)
// build our environment key (eg MATTERBRIDGE_MATTERMOST_WORK_LOGIN)
key := "matterbridge_" + protocol + "_" + account + "_" + typeField.Name
key = strings.ToUpper(key)
// search the environment
res := os.Getenv(key)
// if it exists and the current field is a string
// then update the current field
if res != "" {
fieldVal := protoStruct.Field(i)
if fieldVal.Kind() == reflect.String {
log.Printf("config: overriding %s from env with %s\n", key, res)
fieldVal.Set(reflect.ValueOf(res))
}
}
}
// update the map with the modified Protocol (cfg.Protocol[account] = Protocol)
val.Field(i).SetMapIndex(reflect.ValueOf(account), reflect.ValueOf(protoCfg))
break
}
}
}
func GetIconURL(msg *Message, cfg *Protocol) string {
iconURL := cfg.IconURL
iconURL = strings.Replace(iconURL, "{NICK}", msg.Username, -1)
iconURL = strings.Replace(iconURL, "{BRIDGE}", msg.Origin, -1)
iconURL = strings.Replace(iconURL, "{PROTOCOL}", msg.Protocol, -1)
return iconURL
}
func GetNick(msg *Message, cfg *Protocol) string {
nick := cfg.RemoteNickFormat
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
nick = strings.Replace(nick, "{BRIDGE}", msg.Origin, -1)
nick = strings.Replace(nick, "{PROTOCOL}", msg.Protocol, -1)
return nick
}

153
bridge/discord/discord.go Normal file
View File

@ -0,0 +1,153 @@
package bdiscord
import (
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/bwmarrin/discordgo"
"strings"
)
type bdiscord struct {
c *discordgo.Session
Config *config.Protocol
Remote chan config.Message
protocol string
origin string
Channels []*discordgo.Channel
Nick string
UseChannelID bool
}
var flog *log.Entry
var protocol = "discord"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, origin string, c chan config.Message) *bdiscord {
b := &bdiscord{}
b.Config = &cfg
b.Remote = c
b.protocol = protocol
b.origin = origin
return b
}
func (b *bdiscord) Connect() error {
var err error
flog.Info("Connecting")
b.c, err = discordgo.New(b.Config.Token)
if err != nil {
flog.Debugf("%#v", err)
return err
}
flog.Info("Connection succeeded")
b.c.AddHandler(b.messageCreate)
err = b.c.Open()
if err != nil {
flog.Debugf("%#v", err)
return err
}
guilds, err := b.c.UserGuilds()
if err != nil {
flog.Debugf("%#v", err)
return err
}
userinfo, err := b.c.User("@me")
if err != nil {
flog.Debugf("%#v", err)
return err
}
b.Nick = userinfo.Username
for _, guild := range guilds {
if guild.Name == b.Config.Server {
b.Channels, err = b.c.GuildChannels(guild.ID)
if err != nil {
flog.Debugf("%#v", err)
return err
}
}
}
return nil
}
func (b *bdiscord) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *bdiscord) JoinChannel(channel string) error {
idcheck := strings.Split(channel, "ID:")
if len(idcheck) > 1 {
b.UseChannelID = true
}
return nil
}
func (b *bdiscord) Name() string {
return b.protocol + "." + b.origin
}
func (b *bdiscord) Protocol() string {
return b.protocol
}
func (b *bdiscord) Origin() string {
return b.origin
}
func (b *bdiscord) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
channelID := b.getChannelID(msg.Channel)
if channelID == "" {
flog.Errorf("Could not find channelID for %v", msg.Channel)
return nil
}
nick := config.GetNick(&msg, b.Config)
b.c.ChannelMessageSend(channelID, nick+msg.Text)
return nil
}
func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
// not relay our own messages
if m.Author.Username == b.Nick {
return
}
if len(m.Attachments) > 0 {
for _, attach := range m.Attachments {
m.Content = m.Content + "\n" + attach.URL
}
}
if m.Content == "" {
return
}
flog.Debugf("Sending message from %s on %s to gateway", m.Author.Username, b.FullOrigin())
channelName := b.getChannelName(m.ChannelID)
if b.UseChannelID {
channelName = "ID:" + m.ChannelID
}
b.Remote <- config.Message{Username: m.Author.Username, Text: m.ContentWithMentionsReplaced(), Channel: channelName,
Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin(), Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg"}
}
func (b *bdiscord) getChannelID(name string) string {
idcheck := strings.Split(name, "ID:")
if len(idcheck) > 1 {
return idcheck[1]
}
for _, channel := range b.Channels {
if channel.Name == name {
return channel.ID
}
}
return ""
}
func (b *bdiscord) getChannelName(id string) string {
for _, channel := range b.Channels {
if channel.ID == id {
return channel.Name
}
}
return ""
}

View File

@ -1,65 +1,116 @@
package bgitter
import (
"github.com/42wim/go-gitter"
"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
Config *config.Protocol
Remote chan config.Message
protocol string
origin string
Users []gitter.User
Rooms []gitter.Room
}
type Message struct {
Text string
Channel string
Username string
}
var flog *log.Entry
var protocol = "gitter"
func init() {
flog = log.WithFields(log.Fields{"module": "gitter"})
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(config *config.Config, c chan config.Message) *Bgitter {
func New(cfg config.Protocol, origin string, c chan config.Message) *Bgitter {
b := &Bgitter{}
b.Config = config
b.Config = &cfg
b.Remote = c
b.protocol = protocol
b.origin = origin
return b
}
func (b *Bgitter) Connect() error {
var err error
flog.Info("Trying Gitter connection")
b.c = gitter.New(b.Config.Gitter.Token)
flog.Info("Connecting")
b.c = gitter.New(b.Config.Token)
_, err = b.c.GetUser()
if err != nil {
flog.Debugf("%#v", err)
return err
}
flog.Info("Connection succeeded")
b.setupChannels()
go b.handleGitter()
b.Rooms, _ = b.c.GetRooms()
return nil
}
func (b *Bgitter) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *Bgitter) JoinChannel(channel string) error {
room := channel
roomID := b.getRoomID(room)
if roomID == "" {
return nil
}
user, err := b.c.GetUser()
if err != nil {
return err
}
_, err = b.c.JoinRoom(roomID, user.ID)
if err != nil {
return err
}
users, _ := b.c.GetUsersInRoom(roomID)
b.Users = append(b.Users, users...)
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, "") {
flog.Debugf("Sending message from %s on %s to gateway", ev.Message.From.Username, b.FullOrigin())
b.Remote <- config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room,
Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin(), Avatar: b.getAvatar(ev.Message.From.Username)}
}
case *gitter.GitterConnectionClosed:
flog.Errorf("connection with gitter closed for room %s", room)
}
}
}(stream, room)
return nil
}
func (b *Bgitter) Name() string {
return "gitter"
return b.protocol + "." + b.origin
}
func (b *Bgitter) Protocol() string {
return b.protocol
}
func (b *Bgitter) Origin() string {
return b.origin
}
func (b *Bgitter) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
roomID := b.getRoomID(msg.Channel)
if roomID == "" {
flog.Errorf("Could not find roomID for %v", msg.Channel)
return nil
}
nick := config.GetNick(&msg, b.Config)
// add ZWSP because gitter echoes our own messages
return b.c.SendMessage(roomID, msg.Username+msg.Text+" ")
return b.c.SendMessage(roomID, nick+msg.Text+" ")
}
func (b *Bgitter) getRoomID(channel string) string {
@ -71,40 +122,14 @@ func (b *Bgitter) getRoomID(channel string) string {
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)
func (b *Bgitter) getAvatar(user string) string {
var avatar string
if b.Users != nil {
for _, u := range b.Users {
if user == u.Username {
return u.AvatarURLSmall
}
}
}
return avatar
}

View File

@ -2,179 +2,244 @@ package birc
import (
"crypto/tls"
"fmt"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
ircm "github.com/sorcix/irc"
"github.com/thoj/go-ircevent"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
//type Bridge struct {
type Birc struct {
i *irc.Connection
ircNick string
ircMap map[string]string
Nick string
names map[string][]string
ircIgnoreNicks []string
*config.Config
Config *config.Protocol
origin string
protocol string
Remote chan config.Message
connected chan struct{}
Local chan config.Message // local queue for flood control
}
type FancyLog struct {
irc *log.Entry
}
var flog FancyLog
var flog *log.Entry
var protocol = "irc"
func init() {
flog.irc = log.WithFields(log.Fields{"module": "irc"})
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(config *config.Config, c chan config.Message) *Birc {
func New(cfg config.Protocol, origin string, c chan config.Message) *Birc {
b := &Birc{}
b.Config = config
b.Config = &cfg
b.Nick = b.Config.Nick
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)
b.origin = origin
b.protocol = protocol
b.connected = make(chan struct{})
if b.Config.MessageDelay == 0 {
b.Config.MessageDelay = 1300
}
if b.Config.MessageQueue == 0 {
b.Config.MessageQueue = 30
}
b.Local = make(chan config.Message, b.Config.MessageQueue+10)
return b
}
func (b *Birc) Command(msg *config.Message) string {
switch msg.Text {
case "!users":
b.i.AddCallback(ircm.RPL_ENDOFNAMES, b.endNames)
b.i.SendRaw("NAMES " + msg.Channel)
b.i.ClearCallback(ircm.RPL_ENDOFNAMES)
}
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
flog.Infof("Connecting %s", b.Config.Server)
i := irc.IRC(b.Config.Nick, b.Config.Nick)
if log.GetLevel() == log.DebugLevel {
i.Debug = true
}
i.UseTLS = b.Config.UseTLS
i.UseSASL = b.Config.UseSASL
i.SASLLogin = b.Config.NickServNick
i.SASLPassword = b.Config.NickServPassword
i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.SkipTLSVerify}
if b.Config.Password != "" {
i.Password = b.Config.Password
}
i.AddCallback(ircm.RPL_WELCOME, b.handleNewConnection)
err := i.Connect(b.Config.IRC.Server)
err := i.Connect(b.Config.Server)
if err != nil {
return err
}
flog.irc.Info("Connection succeeded")
b.i = i
select {
case <-b.connected:
flog.Info("Connection succeeded")
case <-time.After(time.Second * 30):
return fmt.Errorf("connection timed out")
}
i.Debug = false
go b.doSend()
return nil
}
func (b *Birc) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *Birc) JoinChannel(channel string) error {
b.i.Join(channel)
return nil
}
func (b *Birc) Name() string {
return "irc"
return b.protocol + "." + b.origin
}
func (b *Birc) Protocol() string {
return b.protocol
}
func (b *Birc) Origin() string {
return b.origin
}
func (b *Birc) Send(msg config.Message) error {
if msg.Origin == "irc" {
flog.Debugf("Receiving %#v", msg)
if msg.FullOrigin == b.FullOrigin() {
return nil
}
if strings.HasPrefix(msg.Text, "!") {
b.Command(&msg)
return nil
}
b.i.Privmsg(msg.Channel, msg.Username+msg.Text)
nick := config.GetNick(&msg, b.Config)
for _, text := range strings.Split(msg.Text, "\n") {
if len(b.Local) < b.Config.MessageQueue {
if len(b.Local) == b.Config.MessageQueue-1 {
text = text + " <message clipped>"
}
b.Local <- config.Message{Text: text, Username: nick, Channel: msg.Channel}
} else {
flog.Debugf("flooding, dropping message (queue at %d)", len(b.Local))
}
}
return nil
}
func (b *Birc) doSend() {
rate := time.Millisecond * time.Duration(b.Config.MessageDelay)
throttle := time.Tick(rate)
for msg := range b.Local {
<-throttle
b.i.Privmsg(msg.Channel, msg.Username+msg.Text)
}
}
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.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost], continued),
Channel: channel, Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
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.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel], continued), Channel: channel,
Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
b.names[channel] = nil
}
func (b *Birc) handleNewConnection(event *irc.Event) {
flog.irc.Info("Registering callbacks")
flog.Debug("Registering callbacks")
i := b.i
b.ircNick = event.Arguments[0]
b.Nick = 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(ircm.RPL_MYINFO, func(e *irc.Event) { flog.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")
flog.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]))
// we are now fully connected
b.connected <- struct{}{}
}
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)
if strings.Contains(event.Message(), "This nickname is registered") && event.Nick == b.Config.NickServNick {
b.i.Privmsg(b.Config.NickServNick, "IDENTIFY "+b.Config.NickServPassword)
} else {
b.handlePrivMsg(event)
}
}
func (b *Birc) handleOther(event *irc.Event) {
flog.irc.Debugf("%#v", event)
switch event.Code {
case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005":
return
}
flog.Debugf("%#v", event.Raw)
}
func (b *Birc) handlePrivMsg(event *irc.Event) {
flog.irc.Debugf("handlePrivMsg() %s %s", event.Nick, event.Message())
// don't forward queries to the bot
if event.Arguments[0] == b.Nick {
return
}
// don't forward message from ourself
if event.Nick == b.Nick {
return
}
flog.Debugf("handlePrivMsg() %s %s %#v", event.Nick, event.Message(), event)
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"}
// strip IRC colors
re := regexp.MustCompile(`[[:cntrl:]](\d+,|)\d+`)
msg = re.ReplaceAllString(msg, "")
flog.Debugf("Sending message from %s on %s to gateway", event.Arguments[0], b.FullOrigin())
b.Remote <- config.Message{Username: event.Nick, Text: msg, Channel: event.Arguments[0], Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
}
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])
flog.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))
flog.Debugf("%s: Topic set by %s [%s]", event.Code, user, time.Unix(t, 0))
}
func (b *Birc) nicksPerRow() int {
return 4
/*
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) {
@ -185,10 +250,13 @@ func (b *Birc) storeNames(event *irc.Event) {
}
func (b *Birc) formatnicks(nicks []string, continued bool) string {
return plainformatter(nicks, b.nicksPerRow())
/*
switch b.Config.Mattermost.NickFormatter {
case "table":
return tableformatter(nicks, b.nicksPerRow(), continued)
default:
return plainformatter(nicks, b.nicksPerRow())
}
*/
}

View File

@ -5,10 +5,8 @@ import (
"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
}
@ -28,32 +26,28 @@ type MMMessage struct {
type Bmattermost struct {
MMhook
MMapi
*config.Config
Plus bool
Config *config.Protocol
Remote chan config.Message
name string
origin string
protocol string
TeamId string
}
type FancyLog struct {
irc *log.Entry
mm *log.Entry
xmpp *log.Entry
}
var flog FancyLog
const Legacy = "legacy"
var flog *log.Entry
var protocol = "mattermost"
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"})
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.Config, c chan config.Message) *Bmattermost {
func New(cfg config.Protocol, origin string, c chan config.Message) *Bmattermost {
b := &Bmattermost{}
b.Config = cfg
b.Config = &cfg
b.origin = origin
b.Remote = c
b.Plus = cfg.General.Plus
b.protocol = "mattermost"
b.name = cfg.Name
b.mmMap = make(map[string]string)
return b
}
@ -63,45 +57,60 @@ func (b *Bmattermost) Command(cmd string) string {
}
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})
if !b.Config.UseAPI {
flog.Info("Connecting webhooks")
b.mh = matterhook.New(b.Config.URL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
BindAddress: b.Config.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)
b.mc = matterclient.New(b.Config.Login, b.Config.Password,
b.Config.Team, b.Config.Server)
b.mc.SkipTLSVerify = b.Config.SkipTLSVerify
b.mc.NoTLS = b.Config.NoTLS
flog.Infof("Connecting %s (team: %s) on %s", b.Config.Login, b.Config.Team, b.Config.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)
}
flog.Info("Connection succeeded")
b.TeamId = b.mc.GetTeamId()
go b.mc.WsReceiver()
}
go b.handleMatter()
return nil
}
func (b *Bmattermost) Name() string {
return "mattermost"
func (b *Bmattermost) FullOrigin() string {
return b.protocol + "." + b.origin
}
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, "")
func (b *Bmattermost) JoinChannel(channel string) error {
// we can only join channels using the API
if b.Config.UseAPI {
return b.mc.JoinChannel(b.mc.GetChannelId(channel, ""))
}
return nil
}
func (b *Bmattermost) SendType(nick string, message string, channel string, mtype string) error {
if b.Config.Mattermost.PrefixMessagesWithNick {
func (b *Bmattermost) Name() string {
return b.protocol + "." + b.origin
}
func (b *Bmattermost) Origin() string {
return b.origin
}
func (b *Bmattermost) Protocol() string {
return b.protocol
}
func (b *Bmattermost) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
nick := config.GetNick(&msg, b.Config)
message := msg.Text
channel := msg.Channel
if b.Config.PrefixMessagesWithNick {
/*if IsMarkup(message) {
message = nick + "\n\n" + message
} else {
@ -109,53 +118,52 @@ func (b *Bmattermost) SendType(nick string, message string, channel string, mtyp
message = nick + " " + message
//}
}
if !b.Plus {
matterMessage := matterhook.OMessage{IconURL: b.Config.Mattermost.IconURL}
if !b.Config.UseAPI {
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.Channel = channel
matterMessage.UserName = nick
matterMessage.Type = mtype
matterMessage.Type = ""
matterMessage.Text = message
err := b.mh.Send(matterMessage)
if err != nil {
flog.mm.Info(err)
flog.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)
flog.Debugf("Choosing API based Mattermost connection: %t", b.Config.UseAPI)
mchan := make(chan *MMMessage)
if b.Plus {
if b.Config.UseAPI {
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"}
}
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.FullOrigin())
b.Remote <- config.Message{Text: message.Text, Username: message.Username, Channel: message.Channel, Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
}
}
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)
// only listen to message from our team
if message.Raw.Event == "posted" && b.mc.User.Username != message.Username && message.Raw.Data["team_id"].(string) == b.TeamId {
flog.Debugf("Receiving from matterclient %#v", message)
m := &MMMessage{}
m.Username = message.Username
m.Channel = message.Channel
m.Text = message.Text
if len(message.Post.Filenames) > 0 {
for _, link := range b.mc.GetPublicLinks(message.Post.Filenames) {
m.Text = m.Text + "\n" + link
}
}
mchan <- m
}
}
@ -164,7 +172,7 @@ func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
func (b *Bmattermost) handleMatterHook(mchan chan *MMMessage) {
for {
message := b.mh.Receive()
flog.mm.Debugf("receiving from matterhook %#v", message)
flog.Debugf("Receiving from matterhook %#v", message)
m := &MMMessage{}
m.Username = message.UserName
m.Text = message.Text

View File

@ -1,6 +1,7 @@
package bslack
import (
"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus"
@ -13,129 +14,188 @@ type MMMessage struct {
Text string
Channel string
Username string
Raw *slack.MessageEvent
}
type bslack struct {
type Bslack struct {
mh *matterhook.Client
sc *slack.Client
// MMapi
*config.Config
Config *config.Protocol
rtm *slack.RTM
Plus bool
Remote chan config.Message
Users []slack.User
protocol string
origin string
si *slack.Info
channels []slack.Channel
}
var flog *log.Entry
var protocol = "slack"
func init() {
flog = log.WithFields(log.Fields{"module": "slack"})
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.Config, c chan config.Message) *bslack {
b := &bslack{}
b.Config = cfg
func New(cfg config.Protocol, origin string, c chan config.Message) *Bslack {
b := &Bslack{}
b.Config = &cfg
b.Remote = c
b.Plus = cfg.Slack.UseAPI
b.protocol = protocol
b.origin = origin
return b
}
func (b *bslack) Command(cmd string) string {
func (b *Bslack) Command(cmd string) string {
return ""
}
func (b *bslack) Connect() error {
if !b.Plus {
b.mh = matterhook.New(b.Config.Slack.URL,
matterhook.Config{BindAddress: b.Config.Slack.BindAddress})
func (b *Bslack) Connect() error {
flog.Info("Connecting")
if !b.Config.UseAPI {
b.mh = matterhook.New(b.Config.URL,
matterhook.Config{BindAddress: b.Config.BindAddress})
} else {
b.sc = slack.New(b.Config.Slack.Token)
flog.Infof("Trying login on slack with Token")
/*
if err != nil {
return err
}
*/
flog.Info("Login ok")
}
b.sc = slack.New(b.Config.Token)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
}
flog.Info("Connection succeeded")
go b.handleSlack()
return nil
}
func (b *bslack) Name() string {
return "slack"
func (b *Bslack) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *bslack) Send(msg config.Message) error {
flog.Infof("slack send %#v", msg)
if msg.Origin != "slack" {
return b.SendType(msg.Username, msg.Text, msg.Channel, "")
func (b *Bslack) JoinChannel(channel string) error {
// we can only join channels using the API
if b.Config.UseAPI {
_, err := b.sc.JoinChannel(channel)
if err != nil {
return err
}
}
return nil
}
func (b *bslack) SendType(nick string, message string, channel string, mtype string) error {
if b.Config.Slack.PrefixMessagesWithNick {
func (b *Bslack) Name() string {
return b.protocol + "." + b.origin
}
func (b *Bslack) Protocol() string {
return b.protocol
}
func (b *Bslack) Origin() string {
return b.origin
}
func (b *Bslack) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg)
if msg.FullOrigin == b.FullOrigin() {
return nil
}
nick := config.GetNick(&msg, b.Config)
message := msg.Text
channel := msg.Channel
if b.Config.PrefixMessagesWithNick {
message = nick + " " + message
}
if !b.Plus {
matterMessage := matterhook.OMessage{IconURL: b.Config.Slack.IconURL}
if !b.Config.UseAPI {
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.Channel = channel
matterMessage.UserName = nick
matterMessage.Type = mtype
matterMessage.Type = ""
matterMessage.Text = message
err := b.mh.Send(matterMessage)
if err != nil {
flog.Info(err)
return err
}
flog.Debug("->slack channel: ", channel, " ", message)
return nil
}
flog.Debugf("sent to slack channel API: %s %s", channel, message)
newmsg := b.rtm.NewOutgoingMessage(message, b.getChannelByName(channel).ID)
schannel, err := b.getChannelByName(channel)
if err != nil {
return err
}
np := slack.NewPostMessageParameters()
if b.Config.PrefixMessagesWithNick == true {
np.AsUser = true
}
np.Username = nick
np.IconURL = config.GetIconURL(&msg, b.Config)
if msg.Avatar != "" {
np.IconURL = msg.Avatar
}
b.sc.PostMessage(schannel.ID, message, np)
/*
newmsg := b.rtm.NewOutgoingMessage(message, schannel.ID)
b.rtm.SendMessage(newmsg)
*/
return nil
}
func (b *bslack) getChannelByName(name string) *slack.Channel {
func (b *Bslack) getAvatar(user string) string {
var avatar string
if b.Users != nil {
for _, u := range b.Users {
if user == u.Name {
return u.Profile.Image48
}
}
}
return avatar
}
func (b *Bslack) getChannelByName(name string) (*slack.Channel, error) {
if b.channels == nil {
return nil
return nil, fmt.Errorf("%s: channel %s not found (no channels found)", b.FullOrigin(), name)
}
for _, channel := range b.channels {
if channel.Name == name {
return &channel
return &channel, nil
}
}
return nil
return nil, fmt.Errorf("%s: channel %s not found", b.FullOrigin(), name)
}
func (b *bslack) handleSlack() {
flog.Infof("Choosing API based slack connection: %t", b.Plus)
func (b *Bslack) handleSlack() {
flog.Debugf("Choosing API based slack connection: %t", b.Config.UseAPI)
mchan := make(chan *MMMessage)
if b.Plus {
if b.Config.UseAPI {
go b.handleSlackClient(mchan)
} else {
go b.handleMatterHook(mchan)
}
time.Sleep(time.Second)
flog.Info("Start listening for Slack messages")
flog.Debug("Start listening for Slack messages")
for message := range mchan {
// do not send messages from ourself
if message.Username == b.si.User.Name {
continue
}
texts := strings.Split(message.Text, "\n")
for _, text := range texts {
flog.Debug("Sending message from " + message.Username + " to " + message.Channel)
b.Remote <- config.Message{Text: text, Username: message.Username, Channel: message.Channel, Origin: "slack"}
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.FullOrigin())
b.Remote <- config.Message{Text: text, Username: message.Username, Channel: message.Channel, Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin(), Avatar: b.getAvatar(message.Username)}
}
}
}
func (b *bslack) handleSlackClient(mchan chan *MMMessage) {
func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
count := 0
for msg := range b.rtm.IncomingEvents {
switch ev := msg.Data.(type) {
case *slack.MessageEvent:
flog.Debugf("%#v", ev)
// ignore first message
if count > 0 {
flog.Debugf("Receiving from slackclient %#v", ev)
//ev.ReplyTo
channel, err := b.rtm.GetChannelInfo(ev.Channel)
if err != nil {
continue
@ -148,18 +208,16 @@ func (b *bslack) handleSlackClient(mchan chan *MMMessage) {
m.Username = user.Name
m.Channel = channel.Name
m.Text = ev.Text
m.Raw = ev
mchan <- m
}
count++
case *slack.OutgoingErrorEvent:
flog.Debugf("%#v", ev.Error())
case *slack.ConnectedEvent:
b.channels = ev.Info.Channels
for _, val := range b.Config.Channel {
channel := b.getChannelByName(val.Slack)
if channel != nil && !channel.IsMember {
flog.Infof("Joining %s", val.Slack)
b.sc.JoinChannel(channel.ID)
}
}
b.si = ev.Info
b.Users, _ = b.sc.GetUsers()
case *slack.InvalidAuthEvent:
flog.Fatalf("Invalid Token %#v", ev)
default:
@ -167,10 +225,10 @@ func (b *bslack) handleSlackClient(mchan chan *MMMessage) {
}
}
func (b *bslack) handleMatterHook(mchan chan *MMMessage) {
func (b *Bslack) handleMatterHook(mchan chan *MMMessage) {
for {
message := b.mh.Receive()
flog.Debugf("receiving from slack %#v", message)
flog.Debugf("receiving from matterhook (slack) %#v", message)
m := &MMMessage{}
m.Username = message.UserName
m.Text = message.Text

View File

@ -12,62 +12,75 @@ import (
type Bxmpp struct {
xc *xmpp.Client
xmppMap map[string]string
*config.Config
Config *config.Protocol
origin string
protocol string
Remote chan config.Message
}
type FancyLog struct {
xmpp *log.Entry
}
type Message struct {
Text string
Channel string
Username string
}
var flog FancyLog
var flog *log.Entry
var protocol = "xmpp"
func init() {
flog.xmpp = log.WithFields(log.Fields{"module": "xmpp"})
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(config *config.Config, c chan config.Message) *Bxmpp {
func New(cfg config.Protocol, origin string, c chan config.Message) *Bxmpp {
b := &Bxmpp{}
b.xmppMap = make(map[string]string)
b.Config = config
b.Config = &cfg
b.protocol = protocol
b.origin = origin
b.Remote = c
return b
}
func (b *Bxmpp) Connect() error {
var err error
flog.xmpp.Info("Trying XMPP connection")
flog.Infof("Connecting %s", b.Config.Server)
b.xc, err = b.createXMPP()
if err != nil {
flog.xmpp.Debugf("%#v", err)
flog.Debugf("%#v", err)
return err
}
flog.xmpp.Info("Connection succeeded")
b.setupChannels()
flog.Info("Connection succeeded")
go b.handleXmpp()
return nil
}
func (b *Bxmpp) FullOrigin() string {
return b.protocol + "." + b.origin
}
func (b *Bxmpp) JoinChannel(channel string) error {
b.xc.JoinMUCNoHistory(channel+"@"+b.Config.Muc, b.Config.Nick)
return nil
}
func (b *Bxmpp) Name() string {
return "xmpp"
return b.protocol + "." + b.origin
}
func (b *Bxmpp) Protocol() string {
return b.protocol
}
func (b *Bxmpp) Origin() string {
return b.origin
}
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})
flog.Debugf("Receiving %#v", msg)
nick := config.GetNick(&msg, b.Config)
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: nick + 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,
Host: b.Config.Server,
User: b.Config.Jid,
Password: b.Config.Password,
NoTLS: true,
StartTLS: true,
//StartTLS: false,
@ -84,13 +97,6 @@ func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
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)
@ -121,9 +127,9 @@ func (b *Bxmpp) handleXmpp() error {
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"}
if nick != b.Config.Nick {
flog.Debugf("Sending message from %s on %s to gateway", nick, b.FullOrigin())
b.Remote <- config.Message{Username: nick, Text: v.Text, Channel: channel, Origin: b.origin, Protocol: b.protocol, FullOrigin: b.FullOrigin()}
}
}
case xmpp.Presence:

View File

@ -1,3 +1,39 @@
# v0.8
Release because of breaking mattermost API changes
## New features
* Supports mattermost v3.5.0
# v0.7
## Breaking config changes from 0.6 to 0.7
Matterbridge now uses TOML configuration (https://github.com/toml-lang/toml)
See matterbridge.toml.sample for an example
## New features
### General
* Allow for bridging the same type of bridge, which means you can eg bridge between multiple mattermosts.
* The bridge is now actually a gateway which has support multiple in and out bridges. (and supports multiple gateways).
* Discord support added. See matterbridge.toml.sample for more information.
* Samechannelgateway support added, easier configuration for 1:1 mapping of protocols with same channel names. #35
* Support for override from environment variables. #50
* Better debugging output.
* discord: New protocol support added. (http://www.discordapp.com)
* mattermost: Support attachments.
* irc: Strip colors. #33
* irc: Anti-flooding support. #40
* irc: Forward channel notices.
## Bugfix
* irc: Split newlines. #37
* irc: Only respond to nick related notices from nickserv.
* irc: Ignore queries send to the bot.
* irc: Ignore messages from ourself.
* irc: Only output the "users on irc information" when asked with "!users".
* irc: Actually wait until connection is complete before saying it is.
* mattermost: Fix mattermost channel joins.
* mattermost: Drop messages not from our team.
* slack: Do not panic on non-existing channels.
* general: Exit when a bridge fails to start.
# v0.6.1
## New features
* Slack support added. See matterbridge.conf.sample for more information

151
gateway/gateway.go Normal file
View File

@ -0,0 +1,151 @@
package gateway
import (
"fmt"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"reflect"
"strings"
)
type Gateway struct {
*config.Config
MyConfig *config.Gateway
Bridges []bridge.Bridge
ChannelsOut map[string][]string
ChannelsIn map[string][]string
ignoreNicks map[string][]string
Name string
}
func New(cfg *config.Config, gateway *config.Gateway) error {
c := make(chan config.Message)
gw := &Gateway{}
gw.Name = gateway.Name
gw.Config = cfg
gw.MyConfig = gateway
exists := make(map[string]bool)
for _, br := range append(gateway.In, gateway.Out...) {
if exists[br.Account] {
continue
}
log.Infof("Starting bridge: %s channel: %s", br.Account, br.Channel)
gw.Bridges = append(gw.Bridges, bridge.New(cfg, &br, c))
exists[br.Account] = true
}
gw.mapChannels()
//TODO fix mapIgnores
//gw.mapIgnores()
exists = make(map[string]bool)
for _, br := range gw.Bridges {
err := br.Connect()
if err != nil {
log.Fatalf("Bridge %s failed to start: %v", br.FullOrigin(), err)
}
for _, channel := range append(gw.ChannelsOut[br.FullOrigin()], gw.ChannelsIn[br.FullOrigin()]...) {
if exists[br.FullOrigin()+channel] {
continue
}
log.Infof("%s: joining %s", br.FullOrigin(), channel)
br.JoinChannel(channel)
exists[br.FullOrigin()+channel] = true
}
}
gw.handleReceive(c)
return nil
}
func (gw *Gateway) handleReceive(c chan config.Message) {
for {
select {
case msg := <-c:
for _, br := range gw.Bridges {
gw.handleMessage(msg, br)
}
}
}
}
func (gw *Gateway) mapChannels() error {
m := make(map[string][]string)
for _, br := range gw.MyConfig.Out {
m[br.Account] = append(m[br.Account], br.Channel)
}
gw.ChannelsOut = m
m = nil
m = make(map[string][]string)
for _, br := range gw.MyConfig.In {
m[br.Account] = append(m[br.Account], br.Channel)
}
gw.ChannelsIn = m
return nil
}
func (gw *Gateway) mapIgnores() {
m := make(map[string][]string)
for _, br := range gw.MyConfig.In {
accInfo := strings.Split(br.Account, ".")
m[br.Account] = strings.Fields(gw.Config.IRC[accInfo[1]].IgnoreNicks)
}
gw.ignoreNicks = m
}
func (gw *Gateway) getDestChannel(msg *config.Message, dest string) []string {
channels := gw.ChannelsIn[msg.FullOrigin]
for _, channel := range channels {
if channel == msg.Channel {
return gw.ChannelsOut[dest]
}
}
return []string{}
}
func (gw *Gateway) handleMessage(msg config.Message, dest bridge.Bridge) {
if gw.ignoreMessage(&msg) {
return
}
originchannel := msg.Channel
channels := gw.getDestChannel(&msg, dest.FullOrigin())
for _, channel := range channels {
// do not send the message to the bridge we come from if also the channel is the same
if msg.FullOrigin == dest.FullOrigin() && channel == originchannel {
continue
}
msg.Channel = channel
if msg.Channel == "" {
log.Debug("empty channel")
return
}
log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.FullOrigin, originchannel, dest.FullOrigin(), channel)
err := dest.Send(msg)
if err != nil {
fmt.Println(err)
}
}
}
func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
// should we discard messages ?
for _, entry := range gw.ignoreNicks[msg.FullOrigin] {
if msg.Username == entry {
return true
}
}
return false
}
func (gw *Gateway) modifyMessage(msg *config.Message, dest bridge.Bridge) {
val := reflect.ValueOf(gw.Config).Elem()
for i := 0; i < val.NumField(); i++ {
typeField := val.Type().Field(i)
// look for the protocol map (both lowercase)
if strings.ToLower(typeField.Name) == dest.Protocol() {
// get the Protocol struct from the map
protoCfg := val.Field(i).MapIndex(reflect.ValueOf(dest.Origin()))
//config.SetNickFormat(msg, protoCfg.Interface().(config.Protocol))
val.Field(i).SetMapIndex(reflect.ValueOf(dest.Origin()), protoCfg)
break
}
}
}

View File

@ -0,0 +1,103 @@
package samechannelgateway
import (
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"strings"
)
type SameChannelGateway struct {
*config.Config
MyConfig *config.SameChannelGateway
Bridges []bridge.Bridge
Channels []string
ignoreNicks map[string][]string
Name string
}
func New(cfg *config.Config, gateway *config.SameChannelGateway) error {
c := make(chan config.Message)
gw := &SameChannelGateway{}
gw.Name = gateway.Name
gw.Config = cfg
gw.MyConfig = gateway
gw.Channels = gateway.Channels
for _, account := range gateway.Accounts {
br := config.Bridge{Account: account}
log.Infof("Starting bridge: %s", account)
gw.Bridges = append(gw.Bridges, bridge.New(cfg, &br, c))
}
for _, br := range gw.Bridges {
err := br.Connect()
if err != nil {
log.Fatalf("Bridge %s failed to start: %v", br.FullOrigin(), err)
}
for _, channel := range gw.Channels {
log.Infof("%s: joining %s", br.FullOrigin(), channel)
br.JoinChannel(channel)
}
}
gw.handleReceive(c)
return nil
}
func (gw *SameChannelGateway) handleReceive(c chan config.Message) {
for {
select {
case msg := <-c:
for _, br := range gw.Bridges {
gw.handleMessage(msg, br)
}
}
}
}
func (gw *SameChannelGateway) handleMessage(msg config.Message, dest bridge.Bridge) {
// is this a configured channel
if !gw.validChannel(msg.Channel) {
return
}
// do not send the message to the bridge we come from if also the channel is the same
if msg.FullOrigin == dest.FullOrigin() {
return
}
gw.modifyMessage(&msg, dest)
log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.FullOrigin, msg.Channel, dest.FullOrigin(), msg.Channel)
err := dest.Send(msg)
if err != nil {
log.Error(err)
}
}
func setNickFormat(msg *config.Message, format string) {
if format == "" {
msg.Username = msg.Protocol + "." + msg.Origin + "-" + msg.Username + ": "
return
}
msg.Username = strings.Replace(format, "{NICK}", msg.Username, -1)
msg.Username = strings.Replace(msg.Username, "{BRIDGE}", msg.Origin, -1)
msg.Username = strings.Replace(msg.Username, "{PROTOCOL}", msg.Protocol, -1)
}
func (gw *SameChannelGateway) modifyMessage(msg *config.Message, dest bridge.Bridge) {
switch dest.Protocol() {
case "irc":
setNickFormat(msg, gw.Config.IRC[dest.Origin()].RemoteNickFormat)
case "mattermost":
setNickFormat(msg, gw.Config.Mattermost[dest.Origin()].RemoteNickFormat)
case "slack":
setNickFormat(msg, gw.Config.Slack[dest.Origin()].RemoteNickFormat)
case "discord":
setNickFormat(msg, gw.Config.Discord[dest.Origin()].RemoteNickFormat)
}
}
func (gw *SameChannelGateway) validChannel(channel string) bool {
for _, c := range gw.Channels {
if c == channel {
return true
}
}
return false
}

View File

@ -38,7 +38,7 @@ NickServPassword="secret"
#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}>
RemoteNickFormat="[{BRIDGE}] <{NICK}> "
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
@ -147,7 +147,7 @@ PrefixMessagesWithNick=false
#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}>
RemoteNickFormat="[{BRIDGE}] <{NICK}> "
#how to format the list of IRC nicks when displayed in mattermost.
#Possible options are "table" and "plain"
@ -183,7 +183,7 @@ IgnoreNicks="spammer1 spammer2"
#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}>
RemoteNickFormat="[{BRIDGE}] <{NICK}> "
###################################################################
#slack section

View File

@ -3,22 +3,22 @@ package main
import (
"flag"
"fmt"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway"
"github.com/42wim/matterbridge/gateway/samechannel"
log "github.com/Sirupsen/logrus"
)
var version = "0.6.1"
var version = "0.8.0"
func init() {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
}
func main() {
flagConfig := flag.String("conf", "matterbridge.conf", "config file")
flagConfig := flag.String("conf", "matterbridge.toml", "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)
@ -31,11 +31,30 @@ func main() {
}
fmt.Println("running version", version)
cfg := config.NewConfig(*flagConfig)
if *flagPlus {
cfg.General.Plus = true
for _, gw := range cfg.SameChannelGateway {
if !gw.Enable {
continue
}
err := bridge.NewBridge(cfg)
fmt.Printf("starting samechannel gateway %#v\n", gw.Name)
go func(gw config.SameChannelGateway) {
err := samechannelgateway.New(cfg, &gw)
if err != nil {
log.Debugf("starting bridge failed %#v", err)
log.Debugf("starting gateway failed %#v", err)
}
}(gw)
}
for _, gw := range cfg.Gateway {
if !gw.Enable {
continue
}
fmt.Printf("starting gateway %#v\n", gw.Name)
go func(gw config.Gateway) {
err := gateway.New(cfg, &gw)
if err != nil {
log.Debugf("starting gateway failed %#v", err)
}
}(gw)
}
select {}
}

373
matterbridge.toml.sample Normal file
View File

@ -0,0 +1,373 @@
#This is configuration for matterbridge.
###################################################################
#IRC section
###################################################################
#REQUIRED to start IRC section
[irc]
#You can configure multiple servers "[irc.name]" or "[irc.name2]"
#In this example we use [irc.freenode]
#REQUIRED
[irc.freenode]
#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
#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
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Nicks you want to ignore.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
#Flood control
#Delay in milliseconds between each message send to the IRC server
#OPTIONAL (default 1300)
MessageDelay=1300
#Maximum amount of messages to hold in queue. If queue is full
#messages will be dropped.
#<clipped> will be add to the message that fills the queue.
#OPTIONAL (default 30)
MessageQueue=30
###################################################################
#XMPP section
###################################################################
[xmpp]
#You can configure multiple servers "[xmpp.name]" or "[xmpp.name2]"
#In this example we use [xmpp.jabber]
#REQUIRED
[xmpp.jabber]
#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]
#You can configure multiple servers "[mattermost.name]" or "[mattermost.name2]"
#In this example we use [mattermost.work]
#REQUIRED
[mattermost.work]
#### Settings for webhook matterbridge.
#### These settings will not be used when useAPI is enabled
#Url is your incoming webhook url as specified in mattermost.
#See account settings - integrations - incoming webhooks on mattermost.
#REQUIRED (unless useAPI=true)
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 (unless useAPI=true)
BindAddress="0.0.0.0:9999"
#Icon that will be showed in mattermost.
#OPTIONAL
IconURL="http://youricon.png"
#### Settings for matterbridge -plus
#### Thse settings will only be used when using the -plus switch.
#### Settings for using matterbridge API
#OPTIONAL
useAPI=false
#The mattermost hostname.
#REQUIRED (when useAPI=true)
Server="yourmattermostserver.domain"
#Your team on mattermost.
#REQUIRED (when useAPI=true)
Team="yourteam"
#login/pass of your bot.
#Use a dedicated user for this and not your own!
#REQUIRED (when useAPI=true)
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
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{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"
###################################################################
#Gitter section
#Best to make a dedicated gitter account for the bot.
###################################################################
[gitter]
#You can configure multiple servers "[gitter.name]" or "[gitter.name2]"
#In this example we use [gitter.myproject]
#REQUIRED
[gitter.myproject]
#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
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
###################################################################
#slack section
###################################################################
[slack]
#You can configure multiple servers "[slack.name]" or "[slack.name2]"
#In this example we use [slack.hobby]
#REQUIRED
[slack.hobby]
#### Settings for webhook matterbridge.
#### These settings will not be used when useAPI is enabled
#Url is your incoming webhook url as specified in slack
#See account settings - integrations - incoming webhooks on slack
#REQUIRED (unless useAPI=true)
URL="https://hooks.slack.com/services/yourhook"
#Address to listen on for outgoing webhook requests from slack
#See account settings - integrations - outgoing webhooks on slack
#This setting will not be used when useAPI is eanbled
#webhooks
#REQUIRED (unless useAPI=true)
BindAddress="0.0.0.0:9999"
#### Settings for using slack API
#OPTIONAL
useAPI=false
#Token to connect with the Slack API
#REQUIRED (when useAPI=true)
Token="yourslacktoken"
#### Shared settings for webhooks and API
#Icon that will be showed in slack
#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
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL
IconURL="https://robohash.org/{NICK}.png?size=48x48"
#Whether to prefix messages from other bridges to mattermost with RemoteNickFormat
#Useful if username overrides for incoming webhooks isn't enabled on the
#slack server. If you set PrefixMessagesWithNick to true, each message
#from bridge to Slack will by default be prefixed by "bridge-" + nick. You can,
#however, modify how the messages appear, by setting (and modifying) RemoteNickFormat
#OPTIONAL (default false)
PrefixMessagesWithNick=false
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#how to format the list of IRC nicks when displayed in slack
#Possible options are "table" and "plain"
#OPTIONAL (default plain)
NickFormatter="plain"
#How many nicks to list per row for formatters that support this.
#OPTIONAL (default 4)
NicksPerRow=4
#Nicks you want to ignore. Messages from those users will not be bridged.
#OPTIONAL
IgnoreNicks="mmbot spammer2"
###################################################################
#discord section
###################################################################
[discord]
#You can configure multiple servers "[discord.name]" or "[discord.name2]"
#In this example we use [discord.game]
#REQUIRED
[discord.game]
#Token to connect with Discord API
#You can get your token by following the instructions on
#https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token
#The "Bot" tag needs to be added before the token
#REQUIRED
Token="Bot Yourtokenhere"
#REQUIRED
Server="yourservername"
#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
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
###################################################################
#Gateway configuration
###################################################################
#You can specify multiple gateways using [[gateway]]
#Each gateway has a [[gateway.in]] and a [[gateway.out]]
#[[gateway.in]] specifies the account and channels we will receive messages from.
#[[gateway.out]] specifies the account and channels we will send the messages
#from [[gateway.in]] to.
#
#Most of the time [[gateway.in]] and [[gateway.out]] are the same if you
#want bidirectional bridging.
#
[[gateway]]
#OPTIONAL (not used for now)
name="gateway1"
#Enable enables this gateway
##OPTIONAL (default false)
enable=true
#[[gateway.in]] specifies the account and channels we will receive messages from.
#The following example bridges between mattermost and irc
[[gateway.in]]
#account specified above
#REQUIRED
account="irc.freenode"
#channel to connect on that account
#How to specify them for the different bridges:
#
#irc - #channel (# is required)
#mattermost - channel (the channel name as seen in the URL, not the displayname)
#gitter - username/room
#xmpp - channel
#slack - channel (the channel name as seen in the URL, not the displayname)
#discord - channel (without the #)
# - ID:123456789 (where 123456789 is the channel ID)
# (https://github.com/42wim/matterbridge/issues/57)
#REQUIRED
channel="#testing"
[[gateway.in]]
account="mattermost.work"
channel="off-topic"
[[gateway.out]]
account="irc.freenode"
channel="#testing"
[[gateway.out]]
account="mattermost.work"
channel="off-topic"
#If you want to do a 1:1 mapping between protocols where the channelnames are the same
#e.g. slack and mattermost you can use the samechannelgateway configuration
#the example configuration below send messages from channel testing on mattermost to
#channel testing on slack and vice versa. (and for the channel testing2 and testing3)
[[samechannelgateway]]
enable = false
accounts = [ "mattermost.work","slack.hobby" ]
channels = [ "testing","testing2","testing3"]

32
matterbridge.toml.simple Normal file
View File

@ -0,0 +1,32 @@
[irc]
[irc.freenode]
Server="irc.freenode.net:6667"
Nick="matterbot"
[mattermost]
[mattermost.work]
useAPI=true
Server="yourmattermostserver.domain"
Team="yourteam"
Login="yourlogin"
Password="yourpass"
PrefixMessagesWithNick=true
[[gateway]]
name="gateway1"
enable=true
[[gateway.in]]
account="irc.freenode"
channel="#testing"
[[gateway.in]]
account="mattermost.work"
channel="off-topic"
[[gateway.out]]
account="irc.freenode"
channel="#testing"
[[gateway.out]]
account="mattermost.work"
channel="off-topic"

View File

@ -125,8 +125,11 @@ func (m *MMClient) Login() error {
if appErr != nil {
d := b.Duration()
m.log.Debug(appErr.DetailedError)
//TODO more generic fix needed
if !strings.Contains(appErr.DetailedError, "connection refused") &&
!strings.Contains(appErr.DetailedError, "invalid character") {
!strings.Contains(appErr.DetailedError, "invalid character") &&
!strings.Contains(appErr.DetailedError, "connection reset by peer") &&
!strings.Contains(appErr.DetailedError, "connection timed out") {
if appErr.Message == "" {
return errors.New(appErr.DetailedError)
}
@ -261,7 +264,7 @@ func (m *MMClient) parseActionPost(rmsg *Message) {
}
rmsg.Username = m.GetUser(data.UserId).Username
rmsg.Channel = m.GetChannelName(data.ChannelId)
rmsg.Team = m.GetTeamName(rmsg.Raw.TeamId)
rmsg.Team = m.GetTeamName(rmsg.Raw.Data["team_id"].(string))
// direct message
if rmsg.Raw.Data["channel_type"] == "D" {
rmsg.Channel = m.GetUser(data.UserId).Username
@ -272,7 +275,7 @@ func (m *MMClient) parseActionPost(rmsg *Message) {
}
func (m *MMClient) UpdateUsers() error {
mmusers, err := m.Client.GetProfilesForDirectMessageList(m.Team.Id)
mmusers, err := m.Client.GetProfiles(0, 1000, "")
if err != nil {
return errors.New(err.DetailedError)
}
@ -302,7 +305,7 @@ 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...) {
for _, channel := range append(*t.Channels, *t.MoreChannels...) {
if channel.Id == channelId {
return channel.Name
}
@ -319,7 +322,7 @@ func (m *MMClient) GetChannelId(name string, teamId string) string {
}
for _, t := range m.OtherTeams {
if t.Id == teamId {
for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
for _, channel := range append(*t.Channels, *t.MoreChannels...) {
if channel.Name == name {
return channel.Id
}
@ -333,7 +336,7 @@ 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...) {
for _, channel := range append(*t.Channels, *t.MoreChannels...) {
if channel.Id == channelId {
return channel.Header
}
@ -351,7 +354,7 @@ func (m *MMClient) PostMessage(channelId string, text string) {
func (m *MMClient) JoinChannel(channelId string) error {
m.RLock()
defer m.RUnlock()
for _, c := range m.Team.Channels.Channels {
for _, c := range *m.Team.Channels {
if c.Id == channelId {
m.log.Debug("Not joining ", channelId, " already joined.")
return nil
@ -394,7 +397,7 @@ func (m *MMClient) GetPublicLink(filename string) string {
if err != nil {
return ""
}
return res.Data.(string)
return res
}
func (m *MMClient) GetPublicLinks(filenames []string) []string {
@ -404,7 +407,7 @@ func (m *MMClient) GetPublicLinks(filenames []string) []string {
if err != nil {
continue
}
output = append(output, res.Data.(string))
output = append(output, res)
}
return output
}
@ -429,15 +432,17 @@ func (m *MMClient) UpdateLastViewed(channelId string) {
}
func (m *MMClient) UsernamesInChannel(channelId string) []string {
ceiRes, err := m.Client.GetChannelExtraInfo(channelId, 5000, "")
res, err := m.Client.GetMyChannelMembers()
if err != nil {
m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelId, err)
return []string{}
}
extra := ceiRes.Data.(*model.ChannelExtra)
members := res.Data.(*model.ChannelMembers)
result := []string{}
for _, member := range extra.Members {
result = append(result, member.Username)
for _, channel := range *members {
if channel.ChannelId == channelId {
result = append(result, m.GetUser(channel.UserId).Username)
}
}
return result
}
@ -497,10 +502,10 @@ func (m *MMClient) GetChannels() []*model.Channel {
defer m.RUnlock()
var channels []*model.Channel
// our primary team channels first
channels = append(channels, m.Team.Channels.Channels...)
channels = append(channels, *m.Team.Channels...)
for _, t := range m.OtherTeams {
if t.Id != m.Team.Id {
channels = append(channels, t.Channels.Channels...)
channels = append(channels, *t.Channels...)
}
}
return channels
@ -512,7 +517,7 @@ func (m *MMClient) GetMoreChannels() []*model.Channel {
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.MoreChannels.Channels...)
channels = append(channels, *t.MoreChannels...)
}
return channels
}
@ -523,8 +528,8 @@ func (m *MMClient) GetTeamFromChannel(channelId string) string {
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.Channels.Channels...)
channels = append(channels, t.MoreChannels.Channels...)
channels = append(channels, *t.Channels...)
channels = append(channels, *t.MoreChannels...)
for _, c := range channels {
if c.Id == channelId {
return t.Id
@ -537,11 +542,13 @@ func (m *MMClient) GetTeamFromChannel(channelId string) string {
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
}
@ -576,6 +583,10 @@ func (m *MMClient) GetStatus(userId string) string {
return "offline"
}
func (m *MMClient) GetTeamId() string {
return m.Team.Id
}
func (m *MMClient) StatusLoop() {
for {
if m.WsQuit {
@ -601,7 +612,6 @@ func (m *MMClient) StatusLoop() {
func (m *MMClient) initUser() error {
m.Lock()
defer m.Unlock()
m.log.Debug("initUser()")
initLoad, err := m.Client.GetInitialLoad()
if err != nil {
return err
@ -610,10 +620,10 @@ func (m *MMClient) initUser() error {
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")
//m.log.Debug("initUser(): loading all team data")
for _, v := range initData.Teams {
m.Client.SetTeamId(v.Id)
mmusers, _ := m.Client.GetProfiles(v.Id, "")
mmusers, _ := m.Client.GetProfiles(0, 1000, "")
t := &Team{Team: v, Users: mmusers.Data.(map[string]*model.User), Id: v.Id}
mmchannels, _ := m.Client.GetChannels("")
t.Channels = mmchannels.Data.(*model.ChannelList)

View File

@ -125,6 +125,24 @@ func (gitter *Gitter) GetRooms() ([]Room, error) {
return rooms, nil
}
// GetUsersInRoom returns the users in the room with the passed id
func (gitter *Gitter) GetUsersInRoom(roomID string) ([]User, error) {
var users []User
response, err := gitter.get(gitter.config.apiBaseURL + "rooms/" + roomID + "/users")
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &users)
if err != nil {
gitter.log(err)
return nil, err
}
return users, nil
}
// GetRoom returns a room with the passed id
func (gitter *Gitter) GetRoom(roomID string) (*Room, error) {
@ -192,7 +210,7 @@ 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)
_, err := gitter.post(gitter.config.apiBaseURL+"rooms/"+roomID+"/chatMessages", body)
if err != nil {
gitter.log(err)
return err
@ -202,31 +220,37 @@ func (gitter *Gitter) SendMessage(roomID, text string) error {
}
// JoinRoom joins a room
func (gitter *Gitter) JoinRoom(uri string) (*Room, error) {
func (gitter *Gitter) JoinRoom(roomID, userID string) (*Room, error) {
message := Room{URI: uri}
message := Room{ID: roomID}
body, _ := json.Marshal(message)
err := gitter.post(apiBaseURL+"rooms", body)
response, err := gitter.post(gitter.config.apiBaseURL+"user/"+userID+"/rooms", body)
if err != nil {
gitter.log(err)
return nil, err
}
rooms, err := gitter.GetRooms()
var room Room
err = json.Unmarshal(response, &room)
if err != nil {
gitter.log(err)
return nil, err
}
for _, room := range rooms {
if room.URI == uri {
return &room, nil
}
// LeaveRoom removes a user from the room
func (gitter *Gitter) LeaveRoom(roomID, userID string) error {
_, err := gitter.delete(gitter.config.apiBaseURL + "rooms/" + roomID + "/users/" + userID)
if err != nil {
gitter.log(err)
return err
}
err = APIError{What: fmt.Sprintf("Joined room (%v) not found in list of rooms", uri)}
gitter.log(err)
return nil, err
return nil
}
// SetDebug traces errors if it's set to true.
@ -319,11 +343,11 @@ func (gitter *Gitter) get(url string) ([]byte, error) {
return body, nil
}
func (gitter *Gitter) post(url string, body []byte) error {
func (gitter *Gitter) post(url string, body []byte) ([]byte, error) {
r, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
gitter.log(err)
return err
return nil, err
}
r.Header.Set("Content-Type", "application/json")
@ -333,17 +357,56 @@ func (gitter *Gitter) post(url string, body []byte) error {
resp, err := gitter.config.client.Do(r)
if err != nil {
gitter.log(err)
return 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 err
return nil, err
}
return nil
result, err := ioutil.ReadAll(resp.Body)
if err != nil {
gitter.log(err)
return nil, err
}
return result, nil
}
func (gitter *Gitter) delete(url string) ([]byte, error) {
r, err := http.NewRequest("delete", 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)
resp, err := gitter.config.client.Do(r)
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
}
result, err := ioutil.ReadAll(resp.Body)
if err != nil {
gitter.log(err)
return nil, err
}
return result, nil
}
func (gitter *Gitter) log(a interface{}) {

View File

@ -57,6 +57,11 @@ Loop:
//"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')
if err != nil {
gitter.log("ReadBytes error: " + err.Error())
stream.connect()
continue
}
//Check if the line only consists of whitespace
onlyWhitespace := true
@ -77,10 +82,6 @@ Loop:
} 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

14
vendor/github.com/BurntSushi/toml/COPYING generated vendored Normal file
View File

@ -0,0 +1,14 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

View File

@ -0,0 +1,90 @@
// Command toml-test-decoder satisfies the toml-test interface for testing
// TOML decoders. Namely, it accepts TOML on stdin and outputs JSON on stdout.
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path"
"time"
"github.com/BurntSushi/toml"
)
func init() {
log.SetFlags(0)
flag.Usage = usage
flag.Parse()
}
func usage() {
log.Printf("Usage: %s < toml-file\n", path.Base(os.Args[0]))
flag.PrintDefaults()
os.Exit(1)
}
func main() {
if flag.NArg() != 0 {
flag.Usage()
}
var tmp interface{}
if _, err := toml.DecodeReader(os.Stdin, &tmp); err != nil {
log.Fatalf("Error decoding TOML: %s", err)
}
typedTmp := translate(tmp)
if err := json.NewEncoder(os.Stdout).Encode(typedTmp); err != nil {
log.Fatalf("Error encoding JSON: %s", err)
}
}
func translate(tomlData interface{}) interface{} {
switch orig := tomlData.(type) {
case map[string]interface{}:
typed := make(map[string]interface{}, len(orig))
for k, v := range orig {
typed[k] = translate(v)
}
return typed
case []map[string]interface{}:
typed := make([]map[string]interface{}, len(orig))
for i, v := range orig {
typed[i] = translate(v).(map[string]interface{})
}
return typed
case []interface{}:
typed := make([]interface{}, len(orig))
for i, v := range orig {
typed[i] = translate(v)
}
// We don't really need to tag arrays, but let's be future proof.
// (If TOML ever supports tuples, we'll need this.)
return tag("array", typed)
case time.Time:
return tag("datetime", orig.Format("2006-01-02T15:04:05Z"))
case bool:
return tag("bool", fmt.Sprintf("%v", orig))
case int64:
return tag("integer", fmt.Sprintf("%d", orig))
case float64:
return tag("float", fmt.Sprintf("%v", orig))
case string:
return tag("string", orig)
}
panic(fmt.Sprintf("Unknown type: %T", tomlData))
}
func tag(typeName string, data interface{}) map[string]interface{} {
return map[string]interface{}{
"type": typeName,
"value": data,
}
}

View File

@ -0,0 +1,131 @@
// Command toml-test-encoder satisfies the toml-test interface for testing
// TOML encoders. Namely, it accepts JSON on stdin and outputs TOML on stdout.
package main
import (
"encoding/json"
"flag"
"log"
"os"
"path"
"strconv"
"time"
"github.com/BurntSushi/toml"
)
func init() {
log.SetFlags(0)
flag.Usage = usage
flag.Parse()
}
func usage() {
log.Printf("Usage: %s < json-file\n", path.Base(os.Args[0]))
flag.PrintDefaults()
os.Exit(1)
}
func main() {
if flag.NArg() != 0 {
flag.Usage()
}
var tmp interface{}
if err := json.NewDecoder(os.Stdin).Decode(&tmp); err != nil {
log.Fatalf("Error decoding JSON: %s", err)
}
tomlData := translate(tmp)
if err := toml.NewEncoder(os.Stdout).Encode(tomlData); err != nil {
log.Fatalf("Error encoding TOML: %s", err)
}
}
func translate(typedJson interface{}) interface{} {
switch v := typedJson.(type) {
case map[string]interface{}:
if len(v) == 2 && in("type", v) && in("value", v) {
return untag(v)
}
m := make(map[string]interface{}, len(v))
for k, v2 := range v {
m[k] = translate(v2)
}
return m
case []interface{}:
tabArray := make([]map[string]interface{}, len(v))
for i := range v {
if m, ok := translate(v[i]).(map[string]interface{}); ok {
tabArray[i] = m
} else {
log.Fatalf("JSON arrays may only contain objects. This " +
"corresponds to only tables being allowed in " +
"TOML table arrays.")
}
}
return tabArray
}
log.Fatalf("Unrecognized JSON format '%T'.", typedJson)
panic("unreachable")
}
func untag(typed map[string]interface{}) interface{} {
t := typed["type"].(string)
v := typed["value"]
switch t {
case "string":
return v.(string)
case "integer":
v := v.(string)
n, err := strconv.Atoi(v)
if err != nil {
log.Fatalf("Could not parse '%s' as integer: %s", v, err)
}
return n
case "float":
v := v.(string)
f, err := strconv.ParseFloat(v, 64)
if err != nil {
log.Fatalf("Could not parse '%s' as float64: %s", v, err)
}
return f
case "datetime":
v := v.(string)
t, err := time.Parse("2006-01-02T15:04:05Z", v)
if err != nil {
log.Fatalf("Could not parse '%s' as a datetime: %s", v, err)
}
return t
case "bool":
v := v.(string)
switch v {
case "true":
return true
case "false":
return false
}
log.Fatalf("Could not parse '%s' as a boolean.", v)
case "array":
v := v.([]interface{})
array := make([]interface{}, len(v))
for i := range v {
if m, ok := v[i].(map[string]interface{}); ok {
array[i] = untag(m)
} else {
log.Fatalf("Arrays may only contain other arrays or "+
"primitive values, but found a '%T'.", m)
}
}
return array
}
log.Fatalf("Unrecognized tag type '%s'.", t)
panic("unreachable")
}
func in(key string, m map[string]interface{}) bool {
_, ok := m[key]
return ok
}

61
vendor/github.com/BurntSushi/toml/cmd/tomlv/main.go generated vendored Normal file
View File

@ -0,0 +1,61 @@
// Command tomlv validates TOML documents and prints each key's type.
package main
import (
"flag"
"fmt"
"log"
"os"
"path"
"strings"
"text/tabwriter"
"github.com/BurntSushi/toml"
)
var (
flagTypes = false
)
func init() {
log.SetFlags(0)
flag.BoolVar(&flagTypes, "types", flagTypes,
"When set, the types of every defined key will be shown.")
flag.Usage = usage
flag.Parse()
}
func usage() {
log.Printf("Usage: %s toml-file [ toml-file ... ]\n",
path.Base(os.Args[0]))
flag.PrintDefaults()
os.Exit(1)
}
func main() {
if flag.NArg() < 1 {
flag.Usage()
}
for _, f := range flag.Args() {
var tmp interface{}
md, err := toml.DecodeFile(f, &tmp)
if err != nil {
log.Fatalf("Error in '%s': %s", f, err)
}
if flagTypes {
printTypes(md)
}
}
}
func printTypes(md toml.MetaData) {
tabw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
for _, key := range md.Keys() {
fmt.Fprintf(tabw, "%s%s\t%s\n",
strings.Repeat(" ", len(key)-1), key, md.Type(key...))
}
tabw.Flush()
}

509
vendor/github.com/BurntSushi/toml/decode.go generated vendored Normal file
View File

@ -0,0 +1,509 @@
package toml
import (
"fmt"
"io"
"io/ioutil"
"math"
"reflect"
"strings"
"time"
)
func e(format string, args ...interface{}) error {
return fmt.Errorf("toml: "+format, args...)
}
// Unmarshaler is the interface implemented by objects that can unmarshal a
// TOML description of themselves.
type Unmarshaler interface {
UnmarshalTOML(interface{}) error
}
// Unmarshal decodes the contents of `p` in TOML format into a pointer `v`.
func Unmarshal(p []byte, v interface{}) error {
_, err := Decode(string(p), v)
return err
}
// Primitive is a TOML value that hasn't been decoded into a Go value.
// When using the various `Decode*` functions, the type `Primitive` may
// be given to any value, and its decoding will be delayed.
//
// A `Primitive` value can be decoded using the `PrimitiveDecode` function.
//
// The underlying representation of a `Primitive` value is subject to change.
// Do not rely on it.
//
// N.B. Primitive values are still parsed, so using them will only avoid
// the overhead of reflection. They can be useful when you don't know the
// exact type of TOML data until run time.
type Primitive struct {
undecoded interface{}
context Key
}
// DEPRECATED!
//
// Use MetaData.PrimitiveDecode instead.
func PrimitiveDecode(primValue Primitive, v interface{}) error {
md := MetaData{decoded: make(map[string]bool)}
return md.unify(primValue.undecoded, rvalue(v))
}
// PrimitiveDecode is just like the other `Decode*` functions, except it
// decodes a TOML value that has already been parsed. Valid primitive values
// can *only* be obtained from values filled by the decoder functions,
// including this method. (i.e., `v` may contain more `Primitive`
// values.)
//
// Meta data for primitive values is included in the meta data returned by
// the `Decode*` functions with one exception: keys returned by the Undecoded
// method will only reflect keys that were decoded. Namely, any keys hidden
// behind a Primitive will be considered undecoded. Executing this method will
// update the undecoded keys in the meta data. (See the example.)
func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
md.context = primValue.context
defer func() { md.context = nil }()
return md.unify(primValue.undecoded, rvalue(v))
}
// Decode will decode the contents of `data` in TOML format into a pointer
// `v`.
//
// TOML hashes correspond to Go structs or maps. (Dealer's choice. They can be
// used interchangeably.)
//
// TOML arrays of tables correspond to either a slice of structs or a slice
// of maps.
//
// TOML datetimes correspond to Go `time.Time` values.
//
// All other TOML types (float, string, int, bool and array) correspond
// to the obvious Go types.
//
// An exception to the above rules is if a type implements the
// encoding.TextUnmarshaler interface. In this case, any primitive TOML value
// (floats, strings, integers, booleans and datetimes) will be converted to
// a byte string and given to the value's UnmarshalText method. See the
// Unmarshaler example for a demonstration with time duration strings.
//
// Key mapping
//
// TOML keys can map to either keys in a Go map or field names in a Go
// struct. The special `toml` struct tag may be used to map TOML keys to
// struct fields that don't match the key name exactly. (See the example.)
// A case insensitive match to struct names will be tried if an exact match
// can't be found.
//
// The mapping between TOML values and Go values is loose. That is, there
// may exist TOML values that cannot be placed into your representation, and
// there may be parts of your representation that do not correspond to
// TOML values. This loose mapping can be made stricter by using the IsDefined
// and/or Undecoded methods on the MetaData returned.
//
// This decoder will not handle cyclic types. If a cyclic type is passed,
// `Decode` will not terminate.
func Decode(data string, v interface{}) (MetaData, error) {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr {
return MetaData{}, e("Decode of non-pointer %s", reflect.TypeOf(v))
}
if rv.IsNil() {
return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v))
}
p, err := parse(data)
if err != nil {
return MetaData{}, err
}
md := MetaData{
p.mapping, p.types, p.ordered,
make(map[string]bool, len(p.ordered)), nil,
}
return md, md.unify(p.mapping, indirect(rv))
}
// DecodeFile is just like Decode, except it will automatically read the
// contents of the file at `fpath` and decode it for you.
func DecodeFile(fpath string, v interface{}) (MetaData, error) {
bs, err := ioutil.ReadFile(fpath)
if err != nil {
return MetaData{}, err
}
return Decode(string(bs), v)
}
// DecodeReader is just like Decode, except it will consume all bytes
// from the reader and decode it for you.
func DecodeReader(r io.Reader, v interface{}) (MetaData, error) {
bs, err := ioutil.ReadAll(r)
if err != nil {
return MetaData{}, err
}
return Decode(string(bs), v)
}
// unify performs a sort of type unification based on the structure of `rv`,
// which is the client representation.
//
// Any type mismatch produces an error. Finding a type that we don't know
// how to handle produces an unsupported type error.
func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
// Special case. Look for a `Primitive` value.
if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() {
// Save the undecoded data and the key context into the primitive
// value.
context := make(Key, len(md.context))
copy(context, md.context)
rv.Set(reflect.ValueOf(Primitive{
undecoded: data,
context: context,
}))
return nil
}
// Special case. Unmarshaler Interface support.
if rv.CanAddr() {
if v, ok := rv.Addr().Interface().(Unmarshaler); ok {
return v.UnmarshalTOML(data)
}
}
// Special case. Handle time.Time values specifically.
// TODO: Remove this code when we decide to drop support for Go 1.1.
// This isn't necessary in Go 1.2 because time.Time satisfies the encoding
// interfaces.
if rv.Type().AssignableTo(rvalue(time.Time{}).Type()) {
return md.unifyDatetime(data, rv)
}
// Special case. Look for a value satisfying the TextUnmarshaler interface.
if v, ok := rv.Interface().(TextUnmarshaler); ok {
return md.unifyText(data, v)
}
// BUG(burntsushi)
// The behavior here is incorrect whenever a Go type satisfies the
// encoding.TextUnmarshaler interface but also corresponds to a TOML
// hash or array. In particular, the unmarshaler should only be applied
// to primitive TOML values. But at this point, it will be applied to
// all kinds of values and produce an incorrect error whenever those values
// are hashes or arrays (including arrays of tables).
k := rv.Kind()
// laziness
if k >= reflect.Int && k <= reflect.Uint64 {
return md.unifyInt(data, rv)
}
switch k {
case reflect.Ptr:
elem := reflect.New(rv.Type().Elem())
err := md.unify(data, reflect.Indirect(elem))
if err != nil {
return err
}
rv.Set(elem)
return nil
case reflect.Struct:
return md.unifyStruct(data, rv)
case reflect.Map:
return md.unifyMap(data, rv)
case reflect.Array:
return md.unifyArray(data, rv)
case reflect.Slice:
return md.unifySlice(data, rv)
case reflect.String:
return md.unifyString(data, rv)
case reflect.Bool:
return md.unifyBool(data, rv)
case reflect.Interface:
// we only support empty interfaces.
if rv.NumMethod() > 0 {
return e("unsupported type %s", rv.Type())
}
return md.unifyAnything(data, rv)
case reflect.Float32:
fallthrough
case reflect.Float64:
return md.unifyFloat64(data, rv)
}
return e("unsupported type %s", rv.Kind())
}
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
tmap, ok := mapping.(map[string]interface{})
if !ok {
if mapping == nil {
return nil
}
return e("type mismatch for %s: expected table but found %T",
rv.Type().String(), mapping)
}
for key, datum := range tmap {
var f *field
fields := cachedTypeFields(rv.Type())
for i := range fields {
ff := &fields[i]
if ff.name == key {
f = ff
break
}
if f == nil && strings.EqualFold(ff.name, key) {
f = ff
}
}
if f != nil {
subv := rv
for _, i := range f.index {
subv = indirect(subv.Field(i))
}
if isUnifiable(subv) {
md.decoded[md.context.add(key).String()] = true
md.context = append(md.context, key)
if err := md.unify(datum, subv); err != nil {
return err
}
md.context = md.context[0 : len(md.context)-1]
} else if f.name != "" {
// Bad user! No soup for you!
return e("cannot write unexported field %s.%s",
rv.Type().String(), f.name)
}
}
}
return nil
}
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
tmap, ok := mapping.(map[string]interface{})
if !ok {
if tmap == nil {
return nil
}
return badtype("map", mapping)
}
if rv.IsNil() {
rv.Set(reflect.MakeMap(rv.Type()))
}
for k, v := range tmap {
md.decoded[md.context.add(k).String()] = true
md.context = append(md.context, k)
rvkey := indirect(reflect.New(rv.Type().Key()))
rvval := reflect.Indirect(reflect.New(rv.Type().Elem()))
if err := md.unify(v, rvval); err != nil {
return err
}
md.context = md.context[0 : len(md.context)-1]
rvkey.SetString(k)
rv.SetMapIndex(rvkey, rvval)
}
return nil
}
func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
datav := reflect.ValueOf(data)
if datav.Kind() != reflect.Slice {
if !datav.IsValid() {
return nil
}
return badtype("slice", data)
}
sliceLen := datav.Len()
if sliceLen != rv.Len() {
return e("expected array length %d; got TOML array of length %d",
rv.Len(), sliceLen)
}
return md.unifySliceArray(datav, rv)
}
func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
datav := reflect.ValueOf(data)
if datav.Kind() != reflect.Slice {
if !datav.IsValid() {
return nil
}
return badtype("slice", data)
}
n := datav.Len()
if rv.IsNil() || rv.Cap() < n {
rv.Set(reflect.MakeSlice(rv.Type(), n, n))
}
rv.SetLen(n)
return md.unifySliceArray(datav, rv)
}
func (md *MetaData) unifySliceArray(data, rv reflect.Value) error {
sliceLen := data.Len()
for i := 0; i < sliceLen; i++ {
v := data.Index(i).Interface()
sliceval := indirect(rv.Index(i))
if err := md.unify(v, sliceval); err != nil {
return err
}
}
return nil
}
func (md *MetaData) unifyDatetime(data interface{}, rv reflect.Value) error {
if _, ok := data.(time.Time); ok {
rv.Set(reflect.ValueOf(data))
return nil
}
return badtype("time.Time", data)
}
func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error {
if s, ok := data.(string); ok {
rv.SetString(s)
return nil
}
return badtype("string", data)
}
func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
if num, ok := data.(float64); ok {
switch rv.Kind() {
case reflect.Float32:
fallthrough
case reflect.Float64:
rv.SetFloat(num)
default:
panic("bug")
}
return nil
}
return badtype("float", data)
}
func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
if num, ok := data.(int64); ok {
if rv.Kind() >= reflect.Int && rv.Kind() <= reflect.Int64 {
switch rv.Kind() {
case reflect.Int, reflect.Int64:
// No bounds checking necessary.
case reflect.Int8:
if num < math.MinInt8 || num > math.MaxInt8 {
return e("value %d is out of range for int8", num)
}
case reflect.Int16:
if num < math.MinInt16 || num > math.MaxInt16 {
return e("value %d is out of range for int16", num)
}
case reflect.Int32:
if num < math.MinInt32 || num > math.MaxInt32 {
return e("value %d is out of range for int32", num)
}
}
rv.SetInt(num)
} else if rv.Kind() >= reflect.Uint && rv.Kind() <= reflect.Uint64 {
unum := uint64(num)
switch rv.Kind() {
case reflect.Uint, reflect.Uint64:
// No bounds checking necessary.
case reflect.Uint8:
if num < 0 || unum > math.MaxUint8 {
return e("value %d is out of range for uint8", num)
}
case reflect.Uint16:
if num < 0 || unum > math.MaxUint16 {
return e("value %d is out of range for uint16", num)
}
case reflect.Uint32:
if num < 0 || unum > math.MaxUint32 {
return e("value %d is out of range for uint32", num)
}
}
rv.SetUint(unum)
} else {
panic("unreachable")
}
return nil
}
return badtype("integer", data)
}
func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error {
if b, ok := data.(bool); ok {
rv.SetBool(b)
return nil
}
return badtype("boolean", data)
}
func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error {
rv.Set(reflect.ValueOf(data))
return nil
}
func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error {
var s string
switch sdata := data.(type) {
case TextMarshaler:
text, err := sdata.MarshalText()
if err != nil {
return err
}
s = string(text)
case fmt.Stringer:
s = sdata.String()
case string:
s = sdata
case bool:
s = fmt.Sprintf("%v", sdata)
case int64:
s = fmt.Sprintf("%d", sdata)
case float64:
s = fmt.Sprintf("%f", sdata)
default:
return badtype("primitive (string-like)", data)
}
if err := v.UnmarshalText([]byte(s)); err != nil {
return err
}
return nil
}
// rvalue returns a reflect.Value of `v`. All pointers are resolved.
func rvalue(v interface{}) reflect.Value {
return indirect(reflect.ValueOf(v))
}
// indirect returns the value pointed to by a pointer.
// Pointers are followed until the value is not a pointer.
// New values are allocated for each nil pointer.
//
// An exception to this rule is if the value satisfies an interface of
// interest to us (like encoding.TextUnmarshaler).
func indirect(v reflect.Value) reflect.Value {
if v.Kind() != reflect.Ptr {
if v.CanSet() {
pv := v.Addr()
if _, ok := pv.Interface().(TextUnmarshaler); ok {
return pv
}
}
return v
}
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
return indirect(reflect.Indirect(v))
}
func isUnifiable(rv reflect.Value) bool {
if rv.CanSet() {
return true
}
if _, ok := rv.Interface().(TextUnmarshaler); ok {
return true
}
return false
}
func badtype(expected string, data interface{}) error {
return e("cannot load TOML value of type %T into a Go %s", data, expected)
}

121
vendor/github.com/BurntSushi/toml/decode_meta.go generated vendored Normal file
View File

@ -0,0 +1,121 @@
package toml
import "strings"
// MetaData allows access to meta information about TOML data that may not
// be inferrable via reflection. In particular, whether a key has been defined
// and the TOML type of a key.
type MetaData struct {
mapping map[string]interface{}
types map[string]tomlType
keys []Key
decoded map[string]bool
context Key // Used only during decoding.
}
// IsDefined returns true if the key given exists in the TOML data. The key
// should be specified hierarchially. e.g.,
//
// // access the TOML key 'a.b.c'
// IsDefined("a", "b", "c")
//
// IsDefined will return false if an empty key given. Keys are case sensitive.
func (md *MetaData) IsDefined(key ...string) bool {
if len(key) == 0 {
return false
}
var hash map[string]interface{}
var ok bool
var hashOrVal interface{} = md.mapping
for _, k := range key {
if hash, ok = hashOrVal.(map[string]interface{}); !ok {
return false
}
if hashOrVal, ok = hash[k]; !ok {
return false
}
}
return true
}
// Type returns a string representation of the type of the key specified.
//
// Type will return the empty string if given an empty key or a key that
// does not exist. Keys are case sensitive.
func (md *MetaData) Type(key ...string) string {
fullkey := strings.Join(key, ".")
if typ, ok := md.types[fullkey]; ok {
return typ.typeString()
}
return ""
}
// Key is the type of any TOML key, including key groups. Use (MetaData).Keys
// to get values of this type.
type Key []string
func (k Key) String() string {
return strings.Join(k, ".")
}
func (k Key) maybeQuotedAll() string {
var ss []string
for i := range k {
ss = append(ss, k.maybeQuoted(i))
}
return strings.Join(ss, ".")
}
func (k Key) maybeQuoted(i int) string {
quote := false
for _, c := range k[i] {
if !isBareKeyChar(c) {
quote = true
break
}
}
if quote {
return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\""
}
return k[i]
}
func (k Key) add(piece string) Key {
newKey := make(Key, len(k)+1)
copy(newKey, k)
newKey[len(k)] = piece
return newKey
}
// Keys returns a slice of every key in the TOML data, including key groups.
// Each key is itself a slice, where the first element is the top of the
// hierarchy and the last is the most specific.
//
// The list will have the same order as the keys appeared in the TOML data.
//
// All keys returned are non-empty.
func (md *MetaData) Keys() []Key {
return md.keys
}
// Undecoded returns all keys that have not been decoded in the order in which
// they appear in the original TOML document.
//
// This includes keys that haven't been decoded because of a Primitive value.
// Once the Primitive value is decoded, the keys will be considered decoded.
//
// Also note that decoding into an empty interface will result in no decoding,
// and so no keys will be considered decoded.
//
// In this sense, the Undecoded keys correspond to keys in the TOML document
// that do not have a concrete type in your representation.
func (md *MetaData) Undecoded() []Key {
undecoded := make([]Key, 0, len(md.keys))
for _, key := range md.keys {
if !md.decoded[key.String()] {
undecoded = append(undecoded, key)
}
}
return undecoded
}

27
vendor/github.com/BurntSushi/toml/doc.go generated vendored Normal file
View File

@ -0,0 +1,27 @@
/*
Package toml provides facilities for decoding and encoding TOML configuration
files via reflection. There is also support for delaying decoding with
the Primitive type, and querying the set of keys in a TOML document with the
MetaData type.
The specification implemented: https://github.com/mojombo/toml
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify
whether a file is a valid TOML document. It can also be used to print the
type of each key in a TOML document.
Testing
There are two important types of tests used for this package. The first is
contained inside '*_test.go' files and uses the standard Go unit testing
framework. These tests are primarily devoted to holistically testing the
decoder and encoder.
The second type of testing is used to verify the implementation's adherence
to the TOML specification. These tests have been factored into their own
project: https://github.com/BurntSushi/toml-test
The reason the tests are in a separate project is so that they can be used by
any implementation of TOML. Namely, it is language agnostic.
*/
package toml

568
vendor/github.com/BurntSushi/toml/encode.go generated vendored Normal file
View File

@ -0,0 +1,568 @@
package toml
import (
"bufio"
"errors"
"fmt"
"io"
"reflect"
"sort"
"strconv"
"strings"
"time"
)
type tomlEncodeError struct{ error }
var (
errArrayMixedElementTypes = errors.New(
"toml: cannot encode array with mixed element types")
errArrayNilElement = errors.New(
"toml: cannot encode array with nil element")
errNonString = errors.New(
"toml: cannot encode a map with non-string key type")
errAnonNonStruct = errors.New(
"toml: cannot encode an anonymous field that is not a struct")
errArrayNoTable = errors.New(
"toml: TOML array element cannot contain a table")
errNoKey = errors.New(
"toml: top-level values must be Go maps or structs")
errAnything = errors.New("") // used in testing
)
var quotedReplacer = strings.NewReplacer(
"\t", "\\t",
"\n", "\\n",
"\r", "\\r",
"\"", "\\\"",
"\\", "\\\\",
)
// Encoder controls the encoding of Go values to a TOML document to some
// io.Writer.
//
// The indentation level can be controlled with the Indent field.
type Encoder struct {
// A single indentation level. By default it is two spaces.
Indent string
// hasWritten is whether we have written any output to w yet.
hasWritten bool
w *bufio.Writer
}
// NewEncoder returns a TOML encoder that encodes Go values to the io.Writer
// given. By default, a single indentation level is 2 spaces.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{
w: bufio.NewWriter(w),
Indent: " ",
}
}
// Encode writes a TOML representation of the Go value to the underlying
// io.Writer. If the value given cannot be encoded to a valid TOML document,
// then an error is returned.
//
// The mapping between Go values and TOML values should be precisely the same
// as for the Decode* functions. Similarly, the TextMarshaler interface is
// supported by encoding the resulting bytes as strings. (If you want to write
// arbitrary binary data then you will need to use something like base64 since
// TOML does not have any binary types.)
//
// When encoding TOML hashes (i.e., Go maps or structs), keys without any
// sub-hashes are encoded first.
//
// If a Go map is encoded, then its keys are sorted alphabetically for
// deterministic output. More control over this behavior may be provided if
// there is demand for it.
//
// Encoding Go values without a corresponding TOML representation---like map
// types with non-string keys---will cause an error to be returned. Similarly
// for mixed arrays/slices, arrays/slices with nil elements, embedded
// non-struct types and nested slices containing maps or structs.
// (e.g., [][]map[string]string is not allowed but []map[string]string is OK
// and so is []map[string][]string.)
func (enc *Encoder) Encode(v interface{}) error {
rv := eindirect(reflect.ValueOf(v))
if err := enc.safeEncode(Key([]string{}), rv); err != nil {
return err
}
return enc.w.Flush()
}
func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) {
defer func() {
if r := recover(); r != nil {
if terr, ok := r.(tomlEncodeError); ok {
err = terr.error
return
}
panic(r)
}
}()
enc.encode(key, rv)
return nil
}
func (enc *Encoder) encode(key Key, rv reflect.Value) {
// Special case. Time needs to be in ISO8601 format.
// Special case. If we can marshal the type to text, then we used that.
// Basically, this prevents the encoder for handling these types as
// generic structs (or whatever the underlying type of a TextMarshaler is).
switch rv.Interface().(type) {
case time.Time, TextMarshaler:
enc.keyEqElement(key, rv)
return
}
k := rv.Kind()
switch k {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64,
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
enc.keyEqElement(key, rv)
case reflect.Array, reflect.Slice:
if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) {
enc.eArrayOfTables(key, rv)
} else {
enc.keyEqElement(key, rv)
}
case reflect.Interface:
if rv.IsNil() {
return
}
enc.encode(key, rv.Elem())
case reflect.Map:
if rv.IsNil() {
return
}
enc.eTable(key, rv)
case reflect.Ptr:
if rv.IsNil() {
return
}
enc.encode(key, rv.Elem())
case reflect.Struct:
enc.eTable(key, rv)
default:
panic(e("unsupported type for key '%s': %s", key, k))
}
}
// eElement encodes any value that can be an array element (primitives and
// arrays).
func (enc *Encoder) eElement(rv reflect.Value) {
switch v := rv.Interface().(type) {
case time.Time:
// Special case time.Time as a primitive. Has to come before
// TextMarshaler below because time.Time implements
// encoding.TextMarshaler, but we need to always use UTC.
enc.wf(v.UTC().Format("2006-01-02T15:04:05Z"))
return
case TextMarshaler:
// Special case. Use text marshaler if it's available for this value.
if s, err := v.MarshalText(); err != nil {
encPanic(err)
} else {
enc.writeQuoted(string(s))
}
return
}
switch rv.Kind() {
case reflect.Bool:
enc.wf(strconv.FormatBool(rv.Bool()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64:
enc.wf(strconv.FormatInt(rv.Int(), 10))
case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64:
enc.wf(strconv.FormatUint(rv.Uint(), 10))
case reflect.Float32:
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32)))
case reflect.Float64:
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64)))
case reflect.Array, reflect.Slice:
enc.eArrayOrSliceElement(rv)
case reflect.Interface:
enc.eElement(rv.Elem())
case reflect.String:
enc.writeQuoted(rv.String())
default:
panic(e("unexpected primitive type: %s", rv.Kind()))
}
}
// By the TOML spec, all floats must have a decimal with at least one
// number on either side.
func floatAddDecimal(fstr string) string {
if !strings.Contains(fstr, ".") {
return fstr + ".0"
}
return fstr
}
func (enc *Encoder) writeQuoted(s string) {
enc.wf("\"%s\"", quotedReplacer.Replace(s))
}
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
length := rv.Len()
enc.wf("[")
for i := 0; i < length; i++ {
elem := rv.Index(i)
enc.eElement(elem)
if i != length-1 {
enc.wf(", ")
}
}
enc.wf("]")
}
func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
if len(key) == 0 {
encPanic(errNoKey)
}
for i := 0; i < rv.Len(); i++ {
trv := rv.Index(i)
if isNil(trv) {
continue
}
panicIfInvalidKey(key)
enc.newline()
enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll())
enc.newline()
enc.eMapOrStruct(key, trv)
}
}
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
panicIfInvalidKey(key)
if len(key) == 1 {
// Output an extra new line between top-level tables.
// (The newline isn't written if nothing else has been written though.)
enc.newline()
}
if len(key) > 0 {
enc.wf("%s[%s]", enc.indentStr(key), key.maybeQuotedAll())
enc.newline()
}
enc.eMapOrStruct(key, rv)
}
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value) {
switch rv := eindirect(rv); rv.Kind() {
case reflect.Map:
enc.eMap(key, rv)
case reflect.Struct:
enc.eStruct(key, rv)
default:
panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String())
}
}
func (enc *Encoder) eMap(key Key, rv reflect.Value) {
rt := rv.Type()
if rt.Key().Kind() != reflect.String {
encPanic(errNonString)
}
// Sort keys so that we have deterministic output. And write keys directly
// underneath this key first, before writing sub-structs or sub-maps.
var mapKeysDirect, mapKeysSub []string
for _, mapKey := range rv.MapKeys() {
k := mapKey.String()
if typeIsHash(tomlTypeOfGo(rv.MapIndex(mapKey))) {
mapKeysSub = append(mapKeysSub, k)
} else {
mapKeysDirect = append(mapKeysDirect, k)
}
}
var writeMapKeys = func(mapKeys []string) {
sort.Strings(mapKeys)
for _, mapKey := range mapKeys {
mrv := rv.MapIndex(reflect.ValueOf(mapKey))
if isNil(mrv) {
// Don't write anything for nil fields.
continue
}
enc.encode(key.add(mapKey), mrv)
}
}
writeMapKeys(mapKeysDirect)
writeMapKeys(mapKeysSub)
}
func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
// Write keys for fields directly under this key first, because if we write
// a field that creates a new table, then all keys under it will be in that
// table (not the one we're writing here).
rt := rv.Type()
var fieldsDirect, fieldsSub [][]int
var addFields func(rt reflect.Type, rv reflect.Value, start []int)
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
// skip unexported fields
if f.PkgPath != "" && !f.Anonymous {
continue
}
frv := rv.Field(i)
if f.Anonymous {
t := f.Type
switch t.Kind() {
case reflect.Struct:
// Treat anonymous struct fields with
// tag names as though they are not
// anonymous, like encoding/json does.
if getOptions(f.Tag).name == "" {
addFields(t, frv, f.Index)
continue
}
case reflect.Ptr:
if t.Elem().Kind() == reflect.Struct &&
getOptions(f.Tag).name == "" {
if !frv.IsNil() {
addFields(t.Elem(), frv.Elem(), f.Index)
}
continue
}
// Fall through to the normal field encoding logic below
// for non-struct anonymous fields.
}
}
if typeIsHash(tomlTypeOfGo(frv)) {
fieldsSub = append(fieldsSub, append(start, f.Index...))
} else {
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
}
}
}
addFields(rt, rv, nil)
var writeFields = func(fields [][]int) {
for _, fieldIndex := range fields {
sft := rt.FieldByIndex(fieldIndex)
sf := rv.FieldByIndex(fieldIndex)
if isNil(sf) {
// Don't write anything for nil fields.
continue
}
opts := getOptions(sft.Tag)
if opts.skip {
continue
}
keyName := sft.Name
if opts.name != "" {
keyName = opts.name
}
if opts.omitempty && isEmpty(sf) {
continue
}
if opts.omitzero && isZero(sf) {
continue
}
enc.encode(key.add(keyName), sf)
}
}
writeFields(fieldsDirect)
writeFields(fieldsSub)
}
// tomlTypeName returns the TOML type name of the Go value's type. It is
// used to determine whether the types of array elements are mixed (which is
// forbidden). If the Go value is nil, then it is illegal for it to be an array
// element, and valueIsNil is returned as true.
// Returns the TOML type of a Go value. The type may be `nil`, which means
// no concrete TOML type could be found.
func tomlTypeOfGo(rv reflect.Value) tomlType {
if isNil(rv) || !rv.IsValid() {
return nil
}
switch rv.Kind() {
case reflect.Bool:
return tomlBool
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64:
return tomlInteger
case reflect.Float32, reflect.Float64:
return tomlFloat
case reflect.Array, reflect.Slice:
if typeEqual(tomlHash, tomlArrayType(rv)) {
return tomlArrayHash
}
return tomlArray
case reflect.Ptr, reflect.Interface:
return tomlTypeOfGo(rv.Elem())
case reflect.String:
return tomlString
case reflect.Map:
return tomlHash
case reflect.Struct:
switch rv.Interface().(type) {
case time.Time:
return tomlDatetime
case TextMarshaler:
return tomlString
default:
return tomlHash
}
default:
panic("unexpected reflect.Kind: " + rv.Kind().String())
}
}
// tomlArrayType returns the element type of a TOML array. The type returned
// may be nil if it cannot be determined (e.g., a nil slice or a zero length
// slize). This function may also panic if it finds a type that cannot be
// expressed in TOML (such as nil elements, heterogeneous arrays or directly
// nested arrays of tables).
func tomlArrayType(rv reflect.Value) tomlType {
if isNil(rv) || !rv.IsValid() || rv.Len() == 0 {
return nil
}
firstType := tomlTypeOfGo(rv.Index(0))
if firstType == nil {
encPanic(errArrayNilElement)
}
rvlen := rv.Len()
for i := 1; i < rvlen; i++ {
elem := rv.Index(i)
switch elemType := tomlTypeOfGo(elem); {
case elemType == nil:
encPanic(errArrayNilElement)
case !typeEqual(firstType, elemType):
encPanic(errArrayMixedElementTypes)
}
}
// If we have a nested array, then we must make sure that the nested
// array contains ONLY primitives.
// This checks arbitrarily nested arrays.
if typeEqual(firstType, tomlArray) || typeEqual(firstType, tomlArrayHash) {
nest := tomlArrayType(eindirect(rv.Index(0)))
if typeEqual(nest, tomlHash) || typeEqual(nest, tomlArrayHash) {
encPanic(errArrayNoTable)
}
}
return firstType
}
type tagOptions struct {
skip bool // "-"
name string
omitempty bool
omitzero bool
}
func getOptions(tag reflect.StructTag) tagOptions {
t := tag.Get("toml")
if t == "-" {
return tagOptions{skip: true}
}
var opts tagOptions
parts := strings.Split(t, ",")
opts.name = parts[0]
for _, s := range parts[1:] {
switch s {
case "omitempty":
opts.omitempty = true
case "omitzero":
opts.omitzero = true
}
}
return opts
}
func isZero(rv reflect.Value) bool {
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return rv.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return rv.Uint() == 0
case reflect.Float32, reflect.Float64:
return rv.Float() == 0.0
}
return false
}
func isEmpty(rv reflect.Value) bool {
switch rv.Kind() {
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
return rv.Len() == 0
case reflect.Bool:
return !rv.Bool()
}
return false
}
func (enc *Encoder) newline() {
if enc.hasWritten {
enc.wf("\n")
}
}
func (enc *Encoder) keyEqElement(key Key, val reflect.Value) {
if len(key) == 0 {
encPanic(errNoKey)
}
panicIfInvalidKey(key)
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
enc.eElement(val)
enc.newline()
}
func (enc *Encoder) wf(format string, v ...interface{}) {
if _, err := fmt.Fprintf(enc.w, format, v...); err != nil {
encPanic(err)
}
enc.hasWritten = true
}
func (enc *Encoder) indentStr(key Key) string {
return strings.Repeat(enc.Indent, len(key)-1)
}
func encPanic(err error) {
panic(tomlEncodeError{err})
}
func eindirect(v reflect.Value) reflect.Value {
switch v.Kind() {
case reflect.Ptr, reflect.Interface:
return eindirect(v.Elem())
default:
return v
}
}
func isNil(rv reflect.Value) bool {
switch rv.Kind() {
case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return rv.IsNil()
default:
return false
}
}
func panicIfInvalidKey(key Key) {
for _, k := range key {
if len(k) == 0 {
encPanic(e("Key '%s' is not a valid table name. Key names "+
"cannot be empty.", key.maybeQuotedAll()))
}
}
}
func isValidKeyName(s string) bool {
return len(s) != 0
}

19
vendor/github.com/BurntSushi/toml/encoding_types.go generated vendored Normal file
View File

@ -0,0 +1,19 @@
// +build go1.2
package toml
// In order to support Go 1.1, we define our own TextMarshaler and
// TextUnmarshaler types. For Go 1.2+, we just alias them with the
// standard library interfaces.
import (
"encoding"
)
// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here
// so that Go 1.1 can be supported.
type TextMarshaler encoding.TextMarshaler
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
// here so that Go 1.1 can be supported.
type TextUnmarshaler encoding.TextUnmarshaler

View File

@ -0,0 +1,18 @@
// +build !go1.2
package toml
// These interfaces were introduced in Go 1.2, so we add them manually when
// compiling for Go 1.1.
// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here
// so that Go 1.1 can be supported.
type TextMarshaler interface {
MarshalText() (text []byte, err error)
}
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
// here so that Go 1.1 can be supported.
type TextUnmarshaler interface {
UnmarshalText(text []byte) error
}

858
vendor/github.com/BurntSushi/toml/lex.go generated vendored Normal file
View File

@ -0,0 +1,858 @@
package toml
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
type itemType int
const (
itemError itemType = iota
itemNIL // used in the parser to indicate no type
itemEOF
itemText
itemString
itemRawString
itemMultilineString
itemRawMultilineString
itemBool
itemInteger
itemFloat
itemDatetime
itemArray // the start of an array
itemArrayEnd
itemTableStart
itemTableEnd
itemArrayTableStart
itemArrayTableEnd
itemKeyStart
itemCommentStart
)
const (
eof = 0
tableStart = '['
tableEnd = ']'
arrayTableStart = '['
arrayTableEnd = ']'
tableSep = '.'
keySep = '='
arrayStart = '['
arrayEnd = ']'
arrayValTerm = ','
commentStart = '#'
stringStart = '"'
stringEnd = '"'
rawStringStart = '\''
rawStringEnd = '\''
)
type stateFn func(lx *lexer) stateFn
type lexer struct {
input string
start int
pos int
width int
line int
state stateFn
items chan item
// A stack of state functions used to maintain context.
// The idea is to reuse parts of the state machine in various places.
// For example, values can appear at the top level or within arbitrarily
// nested arrays. The last state on the stack is used after a value has
// been lexed. Similarly for comments.
stack []stateFn
}
type item struct {
typ itemType
val string
line int
}
func (lx *lexer) nextItem() item {
for {
select {
case item := <-lx.items:
return item
default:
lx.state = lx.state(lx)
}
}
}
func lex(input string) *lexer {
lx := &lexer{
input: input + "\n",
state: lexTop,
line: 1,
items: make(chan item, 10),
stack: make([]stateFn, 0, 10),
}
return lx
}
func (lx *lexer) push(state stateFn) {
lx.stack = append(lx.stack, state)
}
func (lx *lexer) pop() stateFn {
if len(lx.stack) == 0 {
return lx.errorf("BUG in lexer: no states to pop.")
}
last := lx.stack[len(lx.stack)-1]
lx.stack = lx.stack[0 : len(lx.stack)-1]
return last
}
func (lx *lexer) current() string {
return lx.input[lx.start:lx.pos]
}
func (lx *lexer) emit(typ itemType) {
lx.items <- item{typ, lx.current(), lx.line}
lx.start = lx.pos
}
func (lx *lexer) emitTrim(typ itemType) {
lx.items <- item{typ, strings.TrimSpace(lx.current()), lx.line}
lx.start = lx.pos
}
func (lx *lexer) next() (r rune) {
if lx.pos >= len(lx.input) {
lx.width = 0
return eof
}
if lx.input[lx.pos] == '\n' {
lx.line++
}
r, lx.width = utf8.DecodeRuneInString(lx.input[lx.pos:])
lx.pos += lx.width
return r
}
// ignore skips over the pending input before this point.
func (lx *lexer) ignore() {
lx.start = lx.pos
}
// backup steps back one rune. Can be called only once per call of next.
func (lx *lexer) backup() {
lx.pos -= lx.width
if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' {
lx.line--
}
}
// accept consumes the next rune if it's equal to `valid`.
func (lx *lexer) accept(valid rune) bool {
if lx.next() == valid {
return true
}
lx.backup()
return false
}
// peek returns but does not consume the next rune in the input.
func (lx *lexer) peek() rune {
r := lx.next()
lx.backup()
return r
}
// skip ignores all input that matches the given predicate.
func (lx *lexer) skip(pred func(rune) bool) {
for {
r := lx.next()
if pred(r) {
continue
}
lx.backup()
lx.ignore()
return
}
}
// errorf stops all lexing by emitting an error and returning `nil`.
// Note that any value that is a character is escaped if it's a special
// character (new lines, tabs, etc.).
func (lx *lexer) errorf(format string, values ...interface{}) stateFn {
lx.items <- item{
itemError,
fmt.Sprintf(format, values...),
lx.line,
}
return nil
}
// lexTop consumes elements at the top level of TOML data.
func lexTop(lx *lexer) stateFn {
r := lx.next()
if isWhitespace(r) || isNL(r) {
return lexSkip(lx, lexTop)
}
switch r {
case commentStart:
lx.push(lexTop)
return lexCommentStart
case tableStart:
return lexTableStart
case eof:
if lx.pos > lx.start {
return lx.errorf("Unexpected EOF.")
}
lx.emit(itemEOF)
return nil
}
// At this point, the only valid item can be a key, so we back up
// and let the key lexer do the rest.
lx.backup()
lx.push(lexTopEnd)
return lexKeyStart
}
// lexTopEnd is entered whenever a top-level item has been consumed. (A value
// or a table.) It must see only whitespace, and will turn back to lexTop
// upon a new line. If it sees EOF, it will quit the lexer successfully.
func lexTopEnd(lx *lexer) stateFn {
r := lx.next()
switch {
case r == commentStart:
// a comment will read to a new line for us.
lx.push(lexTop)
return lexCommentStart
case isWhitespace(r):
return lexTopEnd
case isNL(r):
lx.ignore()
return lexTop
case r == eof:
lx.ignore()
return lexTop
}
return lx.errorf("Expected a top-level item to end with a new line, "+
"comment or EOF, but got %q instead.", r)
}
// lexTable lexes the beginning of a table. Namely, it makes sure that
// it starts with a character other than '.' and ']'.
// It assumes that '[' has already been consumed.
// It also handles the case that this is an item in an array of tables.
// e.g., '[[name]]'.
func lexTableStart(lx *lexer) stateFn {
if lx.peek() == arrayTableStart {
lx.next()
lx.emit(itemArrayTableStart)
lx.push(lexArrayTableEnd)
} else {
lx.emit(itemTableStart)
lx.push(lexTableEnd)
}
return lexTableNameStart
}
func lexTableEnd(lx *lexer) stateFn {
lx.emit(itemTableEnd)
return lexTopEnd
}
func lexArrayTableEnd(lx *lexer) stateFn {
if r := lx.next(); r != arrayTableEnd {
return lx.errorf("Expected end of table array name delimiter %q, "+
"but got %q instead.", arrayTableEnd, r)
}
lx.emit(itemArrayTableEnd)
return lexTopEnd
}
func lexTableNameStart(lx *lexer) stateFn {
lx.skip(isWhitespace)
switch r := lx.peek(); {
case r == tableEnd || r == eof:
return lx.errorf("Unexpected end of table name. (Table names cannot " +
"be empty.)")
case r == tableSep:
return lx.errorf("Unexpected table separator. (Table names cannot " +
"be empty.)")
case r == stringStart || r == rawStringStart:
lx.ignore()
lx.push(lexTableNameEnd)
return lexValue // reuse string lexing
default:
return lexBareTableName
}
}
// lexBareTableName lexes the name of a table. It assumes that at least one
// valid character for the table has already been read.
func lexBareTableName(lx *lexer) stateFn {
r := lx.next()
if isBareKeyChar(r) {
return lexBareTableName
}
lx.backup()
lx.emit(itemText)
return lexTableNameEnd
}
// lexTableNameEnd reads the end of a piece of a table name, optionally
// consuming whitespace.
func lexTableNameEnd(lx *lexer) stateFn {
lx.skip(isWhitespace)
switch r := lx.next(); {
case isWhitespace(r):
return lexTableNameEnd
case r == tableSep:
lx.ignore()
return lexTableNameStart
case r == tableEnd:
return lx.pop()
default:
return lx.errorf("Expected '.' or ']' to end table name, but got %q "+
"instead.", r)
}
}
// lexKeyStart consumes a key name up until the first non-whitespace character.
// lexKeyStart will ignore whitespace.
func lexKeyStart(lx *lexer) stateFn {
r := lx.peek()
switch {
case r == keySep:
return lx.errorf("Unexpected key separator %q.", keySep)
case isWhitespace(r) || isNL(r):
lx.next()
return lexSkip(lx, lexKeyStart)
case r == stringStart || r == rawStringStart:
lx.ignore()
lx.emit(itemKeyStart)
lx.push(lexKeyEnd)
return lexValue // reuse string lexing
default:
lx.ignore()
lx.emit(itemKeyStart)
return lexBareKey
}
}
// lexBareKey consumes the text of a bare key. Assumes that the first character
// (which is not whitespace) has not yet been consumed.
func lexBareKey(lx *lexer) stateFn {
switch r := lx.next(); {
case isBareKeyChar(r):
return lexBareKey
case isWhitespace(r):
lx.backup()
lx.emit(itemText)
return lexKeyEnd
case r == keySep:
lx.backup()
lx.emit(itemText)
return lexKeyEnd
default:
return lx.errorf("Bare keys cannot contain %q.", r)
}
}
// lexKeyEnd consumes the end of a key and trims whitespace (up to the key
// separator).
func lexKeyEnd(lx *lexer) stateFn {
switch r := lx.next(); {
case r == keySep:
return lexSkip(lx, lexValue)
case isWhitespace(r):
return lexSkip(lx, lexKeyEnd)
default:
return lx.errorf("Expected key separator %q, but got %q instead.",
keySep, r)
}
}
// lexValue starts the consumption of a value anywhere a value is expected.
// lexValue will ignore whitespace.
// After a value is lexed, the last state on the next is popped and returned.
func lexValue(lx *lexer) stateFn {
// We allow whitespace to precede a value, but NOT new lines.
// In array syntax, the array states are responsible for ignoring new
// lines.
r := lx.next()
switch {
case isWhitespace(r):
return lexSkip(lx, lexValue)
case isDigit(r):
lx.backup() // avoid an extra state and use the same as above
return lexNumberOrDateStart
}
switch r {
case arrayStart:
lx.ignore()
lx.emit(itemArray)
return lexArrayValue
case stringStart:
if lx.accept(stringStart) {
if lx.accept(stringStart) {
lx.ignore() // Ignore """
return lexMultilineString
}
lx.backup()
}
lx.ignore() // ignore the '"'
return lexString
case rawStringStart:
if lx.accept(rawStringStart) {
if lx.accept(rawStringStart) {
lx.ignore() // Ignore """
return lexMultilineRawString
}
lx.backup()
}
lx.ignore() // ignore the "'"
return lexRawString
case '+', '-':
return lexNumberStart
case '.': // special error case, be kind to users
return lx.errorf("Floats must start with a digit, not '.'.")
}
if unicode.IsLetter(r) {
// Be permissive here; lexBool will give a nice error if the
// user wrote something like
// x = foo
// (i.e. not 'true' or 'false' but is something else word-like.)
lx.backup()
return lexBool
}
return lx.errorf("Expected value but found %q instead.", r)
}
// lexArrayValue consumes one value in an array. It assumes that '[' or ','
// have already been consumed. All whitespace and new lines are ignored.
func lexArrayValue(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r) || isNL(r):
return lexSkip(lx, lexArrayValue)
case r == commentStart:
lx.push(lexArrayValue)
return lexCommentStart
case r == arrayValTerm:
return lx.errorf("Unexpected array value terminator %q.",
arrayValTerm)
case r == arrayEnd:
return lexArrayEnd
}
lx.backup()
lx.push(lexArrayValueEnd)
return lexValue
}
// lexArrayValueEnd consumes the cruft between values of an array. Namely,
// it ignores whitespace and expects either a ',' or a ']'.
func lexArrayValueEnd(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r) || isNL(r):
return lexSkip(lx, lexArrayValueEnd)
case r == commentStart:
lx.push(lexArrayValueEnd)
return lexCommentStart
case r == arrayValTerm:
lx.ignore()
return lexArrayValue // move on to the next value
case r == arrayEnd:
return lexArrayEnd
}
return lx.errorf("Expected an array value terminator %q or an array "+
"terminator %q, but got %q instead.", arrayValTerm, arrayEnd, r)
}
// lexArrayEnd finishes the lexing of an array. It assumes that a ']' has
// just been consumed.
func lexArrayEnd(lx *lexer) stateFn {
lx.ignore()
lx.emit(itemArrayEnd)
return lx.pop()
}
// lexString consumes the inner contents of a string. It assumes that the
// beginning '"' has already been consumed and ignored.
func lexString(lx *lexer) stateFn {
r := lx.next()
switch {
case isNL(r):
return lx.errorf("Strings cannot contain new lines.")
case r == '\\':
lx.push(lexString)
return lexStringEscape
case r == stringEnd:
lx.backup()
lx.emit(itemString)
lx.next()
lx.ignore()
return lx.pop()
}
return lexString
}
// lexMultilineString consumes the inner contents of a string. It assumes that
// the beginning '"""' has already been consumed and ignored.
func lexMultilineString(lx *lexer) stateFn {
r := lx.next()
switch {
case r == '\\':
return lexMultilineStringEscape
case r == stringEnd:
if lx.accept(stringEnd) {
if lx.accept(stringEnd) {
lx.backup()
lx.backup()
lx.backup()
lx.emit(itemMultilineString)
lx.next()
lx.next()
lx.next()
lx.ignore()
return lx.pop()
}
lx.backup()
}
}
return lexMultilineString
}
// lexRawString consumes a raw string. Nothing can be escaped in such a string.
// It assumes that the beginning "'" has already been consumed and ignored.
func lexRawString(lx *lexer) stateFn {
r := lx.next()
switch {
case isNL(r):
return lx.errorf("Strings cannot contain new lines.")
case r == rawStringEnd:
lx.backup()
lx.emit(itemRawString)
lx.next()
lx.ignore()
return lx.pop()
}
return lexRawString
}
// lexMultilineRawString consumes a raw string. Nothing can be escaped in such
// a string. It assumes that the beginning "'" has already been consumed and
// ignored.
func lexMultilineRawString(lx *lexer) stateFn {
r := lx.next()
switch {
case r == rawStringEnd:
if lx.accept(rawStringEnd) {
if lx.accept(rawStringEnd) {
lx.backup()
lx.backup()
lx.backup()
lx.emit(itemRawMultilineString)
lx.next()
lx.next()
lx.next()
lx.ignore()
return lx.pop()
}
lx.backup()
}
}
return lexMultilineRawString
}
// lexMultilineStringEscape consumes an escaped character. It assumes that the
// preceding '\\' has already been consumed.
func lexMultilineStringEscape(lx *lexer) stateFn {
// Handle the special case first:
if isNL(lx.next()) {
return lexMultilineString
}
lx.backup()
lx.push(lexMultilineString)
return lexStringEscape(lx)
}
func lexStringEscape(lx *lexer) stateFn {
r := lx.next()
switch r {
case 'b':
fallthrough
case 't':
fallthrough
case 'n':
fallthrough
case 'f':
fallthrough
case 'r':
fallthrough
case '"':
fallthrough
case '\\':
return lx.pop()
case 'u':
return lexShortUnicodeEscape
case 'U':
return lexLongUnicodeEscape
}
return lx.errorf("Invalid escape character %q. Only the following "+
"escape characters are allowed: "+
"\\b, \\t, \\n, \\f, \\r, \\\", \\/, \\\\, "+
"\\uXXXX and \\UXXXXXXXX.", r)
}
func lexShortUnicodeEscape(lx *lexer) stateFn {
var r rune
for i := 0; i < 4; i++ {
r = lx.next()
if !isHexadecimal(r) {
return lx.errorf("Expected four hexadecimal digits after '\\u', "+
"but got '%s' instead.", lx.current())
}
}
return lx.pop()
}
func lexLongUnicodeEscape(lx *lexer) stateFn {
var r rune
for i := 0; i < 8; i++ {
r = lx.next()
if !isHexadecimal(r) {
return lx.errorf("Expected eight hexadecimal digits after '\\U', "+
"but got '%s' instead.", lx.current())
}
}
return lx.pop()
}
// lexNumberOrDateStart consumes either an integer, a float, or datetime.
func lexNumberOrDateStart(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexNumberOrDate
}
switch r {
case '_':
return lexNumber
case 'e', 'E':
return lexFloat
case '.':
return lx.errorf("Floats must start with a digit, not '.'.")
}
return lx.errorf("Expected a digit but got %q.", r)
}
// lexNumberOrDate consumes either an integer, float or datetime.
func lexNumberOrDate(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexNumberOrDate
}
switch r {
case '-':
return lexDatetime
case '_':
return lexNumber
case '.', 'e', 'E':
return lexFloat
}
lx.backup()
lx.emit(itemInteger)
return lx.pop()
}
// lexDatetime consumes a Datetime, to a first approximation.
// The parser validates that it matches one of the accepted formats.
func lexDatetime(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexDatetime
}
switch r {
case '-', 'T', ':', '.', 'Z':
return lexDatetime
}
lx.backup()
lx.emit(itemDatetime)
return lx.pop()
}
// lexNumberStart consumes either an integer or a float. It assumes that a sign
// has already been read, but that *no* digits have been consumed.
// lexNumberStart will move to the appropriate integer or float states.
func lexNumberStart(lx *lexer) stateFn {
// We MUST see a digit. Even floats have to start with a digit.
r := lx.next()
if !isDigit(r) {
if r == '.' {
return lx.errorf("Floats must start with a digit, not '.'.")
}
return lx.errorf("Expected a digit but got %q.", r)
}
return lexNumber
}
// lexNumber consumes an integer or a float after seeing the first digit.
func lexNumber(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexNumber
}
switch r {
case '_':
return lexNumber
case '.', 'e', 'E':
return lexFloat
}
lx.backup()
lx.emit(itemInteger)
return lx.pop()
}
// lexFloat consumes the elements of a float. It allows any sequence of
// float-like characters, so floats emitted by the lexer are only a first
// approximation and must be validated by the parser.
func lexFloat(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexFloat
}
switch r {
case '_', '.', '-', '+', 'e', 'E':
return lexFloat
}
lx.backup()
lx.emit(itemFloat)
return lx.pop()
}
// lexBool consumes a bool string: 'true' or 'false.
func lexBool(lx *lexer) stateFn {
var rs []rune
for {
r := lx.next()
if r == eof || isWhitespace(r) || isNL(r) {
lx.backup()
break
}
rs = append(rs, r)
}
s := string(rs)
switch s {
case "true", "false":
lx.emit(itemBool)
return lx.pop()
}
return lx.errorf("Expected value but found %q instead.", s)
}
// lexCommentStart begins the lexing of a comment. It will emit
// itemCommentStart and consume no characters, passing control to lexComment.
func lexCommentStart(lx *lexer) stateFn {
lx.ignore()
lx.emit(itemCommentStart)
return lexComment
}
// lexComment lexes an entire comment. It assumes that '#' has been consumed.
// It will consume *up to* the first new line character, and pass control
// back to the last state on the stack.
func lexComment(lx *lexer) stateFn {
r := lx.peek()
if isNL(r) || r == eof {
lx.emit(itemText)
return lx.pop()
}
lx.next()
return lexComment
}
// lexSkip ignores all slurped input and moves on to the next state.
func lexSkip(lx *lexer, nextState stateFn) stateFn {
return func(lx *lexer) stateFn {
lx.ignore()
return nextState
}
}
// isWhitespace returns true if `r` is a whitespace character according
// to the spec.
func isWhitespace(r rune) bool {
return r == '\t' || r == ' '
}
func isNL(r rune) bool {
return r == '\n' || r == '\r'
}
func isDigit(r rune) bool {
return r >= '0' && r <= '9'
}
func isHexadecimal(r rune) bool {
return (r >= '0' && r <= '9') ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}
func isBareKeyChar(r rune) bool {
return (r >= 'A' && r <= 'Z') ||
(r >= 'a' && r <= 'z') ||
(r >= '0' && r <= '9') ||
r == '_' ||
r == '-'
}
func (itype itemType) String() string {
switch itype {
case itemError:
return "Error"
case itemNIL:
return "NIL"
case itemEOF:
return "EOF"
case itemText:
return "Text"
case itemString, itemRawString, itemMultilineString, itemRawMultilineString:
return "String"
case itemBool:
return "Bool"
case itemInteger:
return "Integer"
case itemFloat:
return "Float"
case itemDatetime:
return "DateTime"
case itemTableStart:
return "TableStart"
case itemTableEnd:
return "TableEnd"
case itemKeyStart:
return "KeyStart"
case itemArray:
return "Array"
case itemArrayEnd:
return "ArrayEnd"
case itemCommentStart:
return "CommentStart"
}
panic(fmt.Sprintf("BUG: Unknown type '%d'.", int(itype)))
}
func (item item) String() string {
return fmt.Sprintf("(%s, %s)", item.typ.String(), item.val)
}

557
vendor/github.com/BurntSushi/toml/parse.go generated vendored Normal file
View File

@ -0,0 +1,557 @@
package toml
import (
"fmt"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
)
type parser struct {
mapping map[string]interface{}
types map[string]tomlType
lx *lexer
// A list of keys in the order that they appear in the TOML data.
ordered []Key
// the full key for the current hash in scope
context Key
// the base key name for everything except hashes
currentKey string
// rough approximation of line number
approxLine int
// A map of 'key.group.names' to whether they were created implicitly.
implicits map[string]bool
}
type parseError string
func (pe parseError) Error() string {
return string(pe)
}
func parse(data string) (p *parser, err error) {
defer func() {
if r := recover(); r != nil {
var ok bool
if err, ok = r.(parseError); ok {
return
}
panic(r)
}
}()
p = &parser{
mapping: make(map[string]interface{}),
types: make(map[string]tomlType),
lx: lex(data),
ordered: make([]Key, 0),
implicits: make(map[string]bool),
}
for {
item := p.next()
if item.typ == itemEOF {
break
}
p.topLevel(item)
}
return p, nil
}
func (p *parser) panicf(format string, v ...interface{}) {
msg := fmt.Sprintf("Near line %d (last key parsed '%s'): %s",
p.approxLine, p.current(), fmt.Sprintf(format, v...))
panic(parseError(msg))
}
func (p *parser) next() item {
it := p.lx.nextItem()
if it.typ == itemError {
p.panicf("%s", it.val)
}
return it
}
func (p *parser) bug(format string, v ...interface{}) {
panic(fmt.Sprintf("BUG: "+format+"\n\n", v...))
}
func (p *parser) expect(typ itemType) item {
it := p.next()
p.assertEqual(typ, it.typ)
return it
}
func (p *parser) assertEqual(expected, got itemType) {
if expected != got {
p.bug("Expected '%s' but got '%s'.", expected, got)
}
}
func (p *parser) topLevel(item item) {
switch item.typ {
case itemCommentStart:
p.approxLine = item.line
p.expect(itemText)
case itemTableStart:
kg := p.next()
p.approxLine = kg.line
var key Key
for ; kg.typ != itemTableEnd && kg.typ != itemEOF; kg = p.next() {
key = append(key, p.keyString(kg))
}
p.assertEqual(itemTableEnd, kg.typ)
p.establishContext(key, false)
p.setType("", tomlHash)
p.ordered = append(p.ordered, key)
case itemArrayTableStart:
kg := p.next()
p.approxLine = kg.line
var key Key
for ; kg.typ != itemArrayTableEnd && kg.typ != itemEOF; kg = p.next() {
key = append(key, p.keyString(kg))
}
p.assertEqual(itemArrayTableEnd, kg.typ)
p.establishContext(key, true)
p.setType("", tomlArrayHash)
p.ordered = append(p.ordered, key)
case itemKeyStart:
kname := p.next()
p.approxLine = kname.line
p.currentKey = p.keyString(kname)
val, typ := p.value(p.next())
p.setValue(p.currentKey, val)
p.setType(p.currentKey, typ)
p.ordered = append(p.ordered, p.context.add(p.currentKey))
p.currentKey = ""
default:
p.bug("Unexpected type at top level: %s", item.typ)
}
}
// Gets a string for a key (or part of a key in a table name).
func (p *parser) keyString(it item) string {
switch it.typ {
case itemText:
return it.val
case itemString, itemMultilineString,
itemRawString, itemRawMultilineString:
s, _ := p.value(it)
return s.(string)
default:
p.bug("Unexpected key type: %s", it.typ)
panic("unreachable")
}
}
// value translates an expected value from the lexer into a Go value wrapped
// as an empty interface.
func (p *parser) value(it item) (interface{}, tomlType) {
switch it.typ {
case itemString:
return p.replaceEscapes(it.val), p.typeOfPrimitive(it)
case itemMultilineString:
trimmed := stripFirstNewline(stripEscapedWhitespace(it.val))
return p.replaceEscapes(trimmed), p.typeOfPrimitive(it)
case itemRawString:
return it.val, p.typeOfPrimitive(it)
case itemRawMultilineString:
return stripFirstNewline(it.val), p.typeOfPrimitive(it)
case itemBool:
switch it.val {
case "true":
return true, p.typeOfPrimitive(it)
case "false":
return false, p.typeOfPrimitive(it)
}
p.bug("Expected boolean value, but got '%s'.", it.val)
case itemInteger:
if !numUnderscoresOK(it.val) {
p.panicf("Invalid integer %q: underscores must be surrounded by digits",
it.val)
}
val := strings.Replace(it.val, "_", "", -1)
num, err := strconv.ParseInt(val, 10, 64)
if err != nil {
// Distinguish integer values. Normally, it'd be a bug if the lexer
// provides an invalid integer, but it's possible that the number is
// out of range of valid values (which the lexer cannot determine).
// So mark the former as a bug but the latter as a legitimate user
// error.
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
p.panicf("Integer '%s' is out of the range of 64-bit "+
"signed integers.", it.val)
} else {
p.bug("Expected integer value, but got '%s'.", it.val)
}
}
return num, p.typeOfPrimitive(it)
case itemFloat:
parts := strings.FieldsFunc(it.val, func(r rune) bool {
switch r {
case '.', 'e', 'E':
return true
}
return false
})
for _, part := range parts {
if !numUnderscoresOK(part) {
p.panicf("Invalid float %q: underscores must be "+
"surrounded by digits", it.val)
}
}
if !numPeriodsOK(it.val) {
// As a special case, numbers like '123.' or '1.e2',
// which are valid as far as Go/strconv are concerned,
// must be rejected because TOML says that a fractional
// part consists of '.' followed by 1+ digits.
p.panicf("Invalid float %q: '.' must be followed "+
"by one or more digits", it.val)
}
val := strings.Replace(it.val, "_", "", -1)
num, err := strconv.ParseFloat(val, 64)
if err != nil {
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
p.panicf("Float '%s' is out of the range of 64-bit "+
"IEEE-754 floating-point numbers.", it.val)
} else {
p.panicf("Invalid float value: %q", it.val)
}
}
return num, p.typeOfPrimitive(it)
case itemDatetime:
var t time.Time
var ok bool
var err error
for _, format := range []string{
"2006-01-02T15:04:05Z07:00",
"2006-01-02T15:04:05",
"2006-01-02",
} {
t, err = time.ParseInLocation(format, it.val, time.Local)
if err == nil {
ok = true
break
}
}
if !ok {
p.panicf("Invalid TOML Datetime: %q.", it.val)
}
return t, p.typeOfPrimitive(it)
case itemArray:
array := make([]interface{}, 0)
types := make([]tomlType, 0)
for it = p.next(); it.typ != itemArrayEnd; it = p.next() {
if it.typ == itemCommentStart {
p.expect(itemText)
continue
}
val, typ := p.value(it)
array = append(array, val)
types = append(types, typ)
}
return array, p.typeOfArray(types)
}
p.bug("Unexpected value type: %s", it.typ)
panic("unreachable")
}
// numUnderscoresOK checks whether each underscore in s is surrounded by
// characters that are not underscores.
func numUnderscoresOK(s string) bool {
accept := false
for _, r := range s {
if r == '_' {
if !accept {
return false
}
accept = false
continue
}
accept = true
}
return accept
}
// numPeriodsOK checks whether every period in s is followed by a digit.
func numPeriodsOK(s string) bool {
period := false
for _, r := range s {
if period && !isDigit(r) {
return false
}
period = r == '.'
}
return !period
}
// establishContext sets the current context of the parser,
// where the context is either a hash or an array of hashes. Which one is
// set depends on the value of the `array` parameter.
//
// Establishing the context also makes sure that the key isn't a duplicate, and
// will create implicit hashes automatically.
func (p *parser) establishContext(key Key, array bool) {
var ok bool
// Always start at the top level and drill down for our context.
hashContext := p.mapping
keyContext := make(Key, 0)
// We only need implicit hashes for key[0:-1]
for _, k := range key[0 : len(key)-1] {
_, ok = hashContext[k]
keyContext = append(keyContext, k)
// No key? Make an implicit hash and move on.
if !ok {
p.addImplicit(keyContext)
hashContext[k] = make(map[string]interface{})
}
// If the hash context is actually an array of tables, then set
// the hash context to the last element in that array.
//
// Otherwise, it better be a table, since this MUST be a key group (by
// virtue of it not being the last element in a key).
switch t := hashContext[k].(type) {
case []map[string]interface{}:
hashContext = t[len(t)-1]
case map[string]interface{}:
hashContext = t
default:
p.panicf("Key '%s' was already created as a hash.", keyContext)
}
}
p.context = keyContext
if array {
// If this is the first element for this array, then allocate a new
// list of tables for it.
k := key[len(key)-1]
if _, ok := hashContext[k]; !ok {
hashContext[k] = make([]map[string]interface{}, 0, 5)
}
// Add a new table. But make sure the key hasn't already been used
// for something else.
if hash, ok := hashContext[k].([]map[string]interface{}); ok {
hashContext[k] = append(hash, make(map[string]interface{}))
} else {
p.panicf("Key '%s' was already created and cannot be used as "+
"an array.", keyContext)
}
} else {
p.setValue(key[len(key)-1], make(map[string]interface{}))
}
p.context = append(p.context, key[len(key)-1])
}
// setValue sets the given key to the given value in the current context.
// It will make sure that the key hasn't already been defined, account for
// implicit key groups.
func (p *parser) setValue(key string, value interface{}) {
var tmpHash interface{}
var ok bool
hash := p.mapping
keyContext := make(Key, 0)
for _, k := range p.context {
keyContext = append(keyContext, k)
if tmpHash, ok = hash[k]; !ok {
p.bug("Context for key '%s' has not been established.", keyContext)
}
switch t := tmpHash.(type) {
case []map[string]interface{}:
// The context is a table of hashes. Pick the most recent table
// defined as the current hash.
hash = t[len(t)-1]
case map[string]interface{}:
hash = t
default:
p.bug("Expected hash to have type 'map[string]interface{}', but "+
"it has '%T' instead.", tmpHash)
}
}
keyContext = append(keyContext, key)
if _, ok := hash[key]; ok {
// Typically, if the given key has already been set, then we have
// to raise an error since duplicate keys are disallowed. However,
// it's possible that a key was previously defined implicitly. In this
// case, it is allowed to be redefined concretely. (See the
// `tests/valid/implicit-and-explicit-after.toml` test in `toml-test`.)
//
// But we have to make sure to stop marking it as an implicit. (So that
// another redefinition provokes an error.)
//
// Note that since it has already been defined (as a hash), we don't
// want to overwrite it. So our business is done.
if p.isImplicit(keyContext) {
p.removeImplicit(keyContext)
return
}
// Otherwise, we have a concrete key trying to override a previous
// key, which is *always* wrong.
p.panicf("Key '%s' has already been defined.", keyContext)
}
hash[key] = value
}
// setType sets the type of a particular value at a given key.
// It should be called immediately AFTER setValue.
//
// Note that if `key` is empty, then the type given will be applied to the
// current context (which is either a table or an array of tables).
func (p *parser) setType(key string, typ tomlType) {
keyContext := make(Key, 0, len(p.context)+1)
for _, k := range p.context {
keyContext = append(keyContext, k)
}
if len(key) > 0 { // allow type setting for hashes
keyContext = append(keyContext, key)
}
p.types[keyContext.String()] = typ
}
// addImplicit sets the given Key as having been created implicitly.
func (p *parser) addImplicit(key Key) {
p.implicits[key.String()] = true
}
// removeImplicit stops tagging the given key as having been implicitly
// created.
func (p *parser) removeImplicit(key Key) {
p.implicits[key.String()] = false
}
// isImplicit returns true if the key group pointed to by the key was created
// implicitly.
func (p *parser) isImplicit(key Key) bool {
return p.implicits[key.String()]
}
// current returns the full key name of the current context.
func (p *parser) current() string {
if len(p.currentKey) == 0 {
return p.context.String()
}
if len(p.context) == 0 {
return p.currentKey
}
return fmt.Sprintf("%s.%s", p.context, p.currentKey)
}
func stripFirstNewline(s string) string {
if len(s) == 0 || s[0] != '\n' {
return s
}
return s[1:]
}
func stripEscapedWhitespace(s string) string {
esc := strings.Split(s, "\\\n")
if len(esc) > 1 {
for i := 1; i < len(esc); i++ {
esc[i] = strings.TrimLeftFunc(esc[i], unicode.IsSpace)
}
}
return strings.Join(esc, "")
}
func (p *parser) replaceEscapes(str string) string {
var replaced []rune
s := []byte(str)
r := 0
for r < len(s) {
if s[r] != '\\' {
c, size := utf8.DecodeRune(s[r:])
r += size
replaced = append(replaced, c)
continue
}
r += 1
if r >= len(s) {
p.bug("Escape sequence at end of string.")
return ""
}
switch s[r] {
default:
p.bug("Expected valid escape code after \\, but got %q.", s[r])
return ""
case 'b':
replaced = append(replaced, rune(0x0008))
r += 1
case 't':
replaced = append(replaced, rune(0x0009))
r += 1
case 'n':
replaced = append(replaced, rune(0x000A))
r += 1
case 'f':
replaced = append(replaced, rune(0x000C))
r += 1
case 'r':
replaced = append(replaced, rune(0x000D))
r += 1
case '"':
replaced = append(replaced, rune(0x0022))
r += 1
case '\\':
replaced = append(replaced, rune(0x005C))
r += 1
case 'u':
// At this point, we know we have a Unicode escape of the form
// `uXXXX` at [r, r+5). (Because the lexer guarantees this
// for us.)
escaped := p.asciiEscapeToUnicode(s[r+1 : r+5])
replaced = append(replaced, escaped)
r += 5
case 'U':
// At this point, we know we have a Unicode escape of the form
// `uXXXX` at [r, r+9). (Because the lexer guarantees this
// for us.)
escaped := p.asciiEscapeToUnicode(s[r+1 : r+9])
replaced = append(replaced, escaped)
r += 9
}
}
return string(replaced)
}
func (p *parser) asciiEscapeToUnicode(bs []byte) rune {
s := string(bs)
hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32)
if err != nil {
p.bug("Could not parse '%s' as a hexadecimal number, but the "+
"lexer claims it's OK: %s", s, err)
}
if !utf8.ValidRune(rune(hex)) {
p.panicf("Escaped character '\\u%s' is not valid UTF-8.", s)
}
return rune(hex)
}
func isStringType(ty itemType) bool {
return ty == itemString || ty == itemMultilineString ||
ty == itemRawString || ty == itemRawMultilineString
}

91
vendor/github.com/BurntSushi/toml/type_check.go generated vendored Normal file
View File

@ -0,0 +1,91 @@
package toml
// tomlType represents any Go type that corresponds to a TOML type.
// While the first draft of the TOML spec has a simplistic type system that
// probably doesn't need this level of sophistication, we seem to be militating
// toward adding real composite types.
type tomlType interface {
typeString() string
}
// typeEqual accepts any two types and returns true if they are equal.
func typeEqual(t1, t2 tomlType) bool {
if t1 == nil || t2 == nil {
return false
}
return t1.typeString() == t2.typeString()
}
func typeIsHash(t tomlType) bool {
return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash)
}
type tomlBaseType string
func (btype tomlBaseType) typeString() string {
return string(btype)
}
func (btype tomlBaseType) String() string {
return btype.typeString()
}
var (
tomlInteger tomlBaseType = "Integer"
tomlFloat tomlBaseType = "Float"
tomlDatetime tomlBaseType = "Datetime"
tomlString tomlBaseType = "String"
tomlBool tomlBaseType = "Bool"
tomlArray tomlBaseType = "Array"
tomlHash tomlBaseType = "Hash"
tomlArrayHash tomlBaseType = "ArrayHash"
)
// typeOfPrimitive returns a tomlType of any primitive value in TOML.
// Primitive values are: Integer, Float, Datetime, String and Bool.
//
// Passing a lexer item other than the following will cause a BUG message
// to occur: itemString, itemBool, itemInteger, itemFloat, itemDatetime.
func (p *parser) typeOfPrimitive(lexItem item) tomlType {
switch lexItem.typ {
case itemInteger:
return tomlInteger
case itemFloat:
return tomlFloat
case itemDatetime:
return tomlDatetime
case itemString:
return tomlString
case itemMultilineString:
return tomlString
case itemRawString:
return tomlString
case itemRawMultilineString:
return tomlString
case itemBool:
return tomlBool
}
p.bug("Cannot infer primitive type of lex item '%s'.", lexItem)
panic("unreachable")
}
// typeOfArray returns a tomlType for an array given a list of types of its
// values.
//
// In the current spec, if an array is homogeneous, then its type is always
// "Array". If the array is not homogeneous, an error is generated.
func (p *parser) typeOfArray(types []tomlType) tomlType {
// Empty arrays are cool.
if len(types) == 0 {
return tomlArray
}
theType := types[0]
for _, t := range types[1:] {
if !typeEqual(theType, t) {
p.panicf("Array contains values of type '%s' and '%s', but "+
"arrays must be homogeneous.", theType, t)
}
}
return tomlArray
}

242
vendor/github.com/BurntSushi/toml/type_fields.go generated vendored Normal file
View File

@ -0,0 +1,242 @@
package toml
// Struct field handling is adapted from code in encoding/json:
//
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the Go distribution.
import (
"reflect"
"sort"
"sync"
)
// A field represents a single field found in a struct.
type field struct {
name string // the name of the field (`toml` tag included)
tag bool // whether field has a `toml` tag
index []int // represents the depth of an anonymous field
typ reflect.Type // the type of the field
}
// byName sorts field by name, breaking ties with depth,
// then breaking ties with "name came from toml tag", then
// breaking ties with index sequence.
type byName []field
func (x byName) Len() int { return len(x) }
func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byName) Less(i, j int) bool {
if x[i].name != x[j].name {
return x[i].name < x[j].name
}
if len(x[i].index) != len(x[j].index) {
return len(x[i].index) < len(x[j].index)
}
if x[i].tag != x[j].tag {
return x[i].tag
}
return byIndex(x).Less(i, j)
}
// byIndex sorts field by index sequence.
type byIndex []field
func (x byIndex) Len() int { return len(x) }
func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byIndex) Less(i, j int) bool {
for k, xik := range x[i].index {
if k >= len(x[j].index) {
return false
}
if xik != x[j].index[k] {
return xik < x[j].index[k]
}
}
return len(x[i].index) < len(x[j].index)
}
// typeFields returns a list of fields that TOML should recognize for the given
// type. The algorithm is breadth-first search over the set of structs to
// include - the top struct and then any reachable anonymous structs.
func typeFields(t reflect.Type) []field {
// Anonymous fields to explore at the current level and the next.
current := []field{}
next := []field{{typ: t}}
// Count of queued names for current level and the next.
count := map[reflect.Type]int{}
nextCount := map[reflect.Type]int{}
// Types already visited at an earlier level.
visited := map[reflect.Type]bool{}
// Fields found.
var fields []field
for len(next) > 0 {
current, next = next, current[:0]
count, nextCount = nextCount, map[reflect.Type]int{}
for _, f := range current {
if visited[f.typ] {
continue
}
visited[f.typ] = true
// Scan f.typ for fields to include.
for i := 0; i < f.typ.NumField(); i++ {
sf := f.typ.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
opts := getOptions(sf.Tag)
if opts.skip {
continue
}
index := make([]int, len(f.index)+1)
copy(index, f.index)
index[len(f.index)] = i
ft := sf.Type
if ft.Name() == "" && ft.Kind() == reflect.Ptr {
// Follow pointer.
ft = ft.Elem()
}
// Record found field and index sequence.
if opts.name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
tagged := opts.name != ""
name := opts.name
if name == "" {
name = sf.Name
}
fields = append(fields, field{name, tagged, index, ft})
if count[f.typ] > 1 {
// If there were multiple instances, add a second,
// so that the annihilation code will see a duplicate.
// It only cares about the distinction between 1 or 2,
// so don't bother generating any more copies.
fields = append(fields, fields[len(fields)-1])
}
continue
}
// Record new anonymous struct to explore in next round.
nextCount[ft]++
if nextCount[ft] == 1 {
f := field{name: ft.Name(), index: index, typ: ft}
next = append(next, f)
}
}
}
}
sort.Sort(byName(fields))
// Delete all fields that are hidden by the Go rules for embedded fields,
// except that fields with TOML tags are promoted.
// The fields are sorted in primary order of name, secondary order
// of field index length. Loop over names; for each name, delete
// hidden fields by choosing the one dominant field that survives.
out := fields[:0]
for advance, i := 0, 0; i < len(fields); i += advance {
// One iteration per name.
// Find the sequence of fields with the name of this first field.
fi := fields[i]
name := fi.name
for advance = 1; i+advance < len(fields); advance++ {
fj := fields[i+advance]
if fj.name != name {
break
}
}
if advance == 1 { // Only one field with this name
out = append(out, fi)
continue
}
dominant, ok := dominantField(fields[i : i+advance])
if ok {
out = append(out, dominant)
}
}
fields = out
sort.Sort(byIndex(fields))
return fields
}
// dominantField looks through the fields, all of which are known to
// have the same name, to find the single field that dominates the
// others using Go's embedding rules, modified by the presence of
// TOML tags. If there are multiple top-level fields, the boolean
// will be false: This condition is an error in Go and we skip all
// the fields.
func dominantField(fields []field) (field, bool) {
// The fields are sorted in increasing index-length order. The winner
// must therefore be one with the shortest index length. Drop all
// longer entries, which is easy: just truncate the slice.
length := len(fields[0].index)
tagged := -1 // Index of first tagged field.
for i, f := range fields {
if len(f.index) > length {
fields = fields[:i]
break
}
if f.tag {
if tagged >= 0 {
// Multiple tagged fields at the same level: conflict.
// Return no field.
return field{}, false
}
tagged = i
}
}
if tagged >= 0 {
return fields[tagged], true
}
// All remaining fields have the same length. If there's more than one,
// we have a conflict (two fields named "X" at the same level) and we
// return no field.
if len(fields) > 1 {
return field{}, false
}
return fields[0], true
}
var fieldCache struct {
sync.RWMutex
m map[reflect.Type][]field
}
// cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
func cachedTypeFields(t reflect.Type) []field {
fieldCache.RLock()
f := fieldCache.m[t]
fieldCache.RUnlock()
if f != nil {
return f
}
// Compute fields without lock.
// Might duplicate effort but won't hold other computations back.
f = typeFields(t)
if f == nil {
f = []field{}
}
fieldCache.Lock()
if fieldCache.m == nil {
fieldCache.m = map[reflect.Type][]field{}
}
fieldCache.m[t] = f
fieldCache.Unlock()
return f
}

28
vendor/github.com/bwmarrin/discordgo/LICENSE generated vendored Normal file
View File

@ -0,0 +1,28 @@
Copyright (c) 2015, Bruce Marriner
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 discordgo nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

257
vendor/github.com/bwmarrin/discordgo/discord.go generated vendored Normal file
View File

@ -0,0 +1,257 @@
// Discordgo - Discord bindings for Go
// Available at https://github.com/bwmarrin/discordgo
// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains high level helper functions and easy entry points for the
// entire discordgo package. These functions are beling developed and are very
// experimental at this point. They will most likley change so please use the
// low level functions if that's a problem.
// Package discordgo provides Discord binding for Go
package discordgo
import (
"fmt"
"reflect"
)
// VERSION of Discordgo, follows Symantic Versioning. (http://semver.org/)
const VERSION = "0.13.0"
// New creates a new Discord session and will automate some startup
// tasks if given enough information to do so. Currently you can pass zero
// arguments and it will return an empty Discord session.
// There are 3 ways to call New:
// With a single auth token - All requests will use the token blindly,
// no verification of the token will be done and requests may fail.
// With an email and password - Discord will sign in with the provided
// credentials.
// With an email, password and auth token - Discord will verify the auth
// token, if it is invalid it will sign in with the provided
// credentials. This is the Discord recommended way to sign in.
func New(args ...interface{}) (s *Session, err error) {
// Create an empty Session interface.
s = &Session{
State: NewState(),
StateEnabled: true,
Compress: true,
ShouldReconnectOnError: true,
ShardID: 0,
ShardCount: 1,
}
// If no arguments are passed return the empty Session interface.
if args == nil {
return
}
// Variables used below when parsing func arguments
var auth, pass string
// Parse passed arguments
for _, arg := range args {
switch v := arg.(type) {
case []string:
if len(v) > 3 {
err = fmt.Errorf("Too many string parameters provided.")
return
}
// First string is either token or username
if len(v) > 0 {
auth = v[0]
}
// If second string exists, it must be a password.
if len(v) > 1 {
pass = v[1]
}
// If third string exists, it must be an auth token.
if len(v) > 2 {
s.Token = v[2]
}
case string:
// First string must be either auth token or username.
// Second string must be a password.
// Only 2 input strings are supported.
if auth == "" {
auth = v
} else if pass == "" {
pass = v
} else if s.Token == "" {
s.Token = v
} else {
err = fmt.Errorf("Too many string parameters provided.")
return
}
// case Config:
// TODO: Parse configuration struct
default:
err = fmt.Errorf("Unsupported parameter type provided.")
return
}
}
// If only one string was provided, assume it is an auth token.
// Otherwise get auth token from Discord, if a token was specified
// Discord will verify it for free, or log the user in if it is
// invalid.
if pass == "" {
s.Token = auth
} else {
err = s.Login(auth, pass)
if err != nil || s.Token == "" {
err = fmt.Errorf("Unable to fetch discord authentication token. %v", err)
return
}
}
// The Session is now able to have RestAPI methods called on it.
// It is recommended that you now call Open() so that events will trigger.
return
}
// validateHandler takes an event handler func, and returns the type of event.
// eg.
// Session.validateHandler(func (s *discordgo.Session, m *discordgo.MessageCreate))
// will return the reflect.Type of *discordgo.MessageCreate
func (s *Session) validateHandler(handler interface{}) reflect.Type {
handlerType := reflect.TypeOf(handler)
if handlerType.NumIn() != 2 {
panic("Unable to add event handler, handler must be of the type func(*discordgo.Session, *discordgo.EventType).")
}
if handlerType.In(0) != reflect.TypeOf(s) {
panic("Unable to add event handler, first argument must be of type *discordgo.Session.")
}
eventType := handlerType.In(1)
// Support handlers of type interface{}, this is a special handler, which is triggered on every event.
if eventType.Kind() == reflect.Interface {
eventType = nil
}
return eventType
}
// AddHandler allows you to add an event handler that will be fired anytime
// the Discord WSAPI event that matches the interface fires.
// eventToInterface in events.go has a list of all the Discord WSAPI events
// and their respective interface.
// eg:
// Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
// })
//
// or:
// Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) {
// })
// The return value of this method is a function, that when called will remove the
// event handler.
func (s *Session) AddHandler(handler interface{}) func() {
s.initialize()
eventType := s.validateHandler(handler)
s.handlersMu.Lock()
defer s.handlersMu.Unlock()
h := reflect.ValueOf(handler)
s.handlers[eventType] = append(s.handlers[eventType], h)
// This must be done as we need a consistent reference to the
// reflected value, otherwise a RemoveHandler method would have
// been nice.
return func() {
s.handlersMu.Lock()
defer s.handlersMu.Unlock()
handlers := s.handlers[eventType]
for i, v := range handlers {
if h == v {
s.handlers[eventType] = append(handlers[:i], handlers[i+1:]...)
return
}
}
}
}
// handle calls any handlers that match the event type and any handlers of
// interface{}.
func (s *Session) handle(event interface{}) {
s.handlersMu.RLock()
defer s.handlersMu.RUnlock()
if s.handlers == nil {
return
}
handlerParameters := []reflect.Value{reflect.ValueOf(s), reflect.ValueOf(event)}
if handlers, ok := s.handlers[nil]; ok {
for _, handler := range handlers {
go handler.Call(handlerParameters)
}
}
if handlers, ok := s.handlers[reflect.TypeOf(event)]; ok {
for _, handler := range handlers {
go handler.Call(handlerParameters)
}
}
}
// initialize adds all internal handlers and state tracking handlers.
func (s *Session) initialize() {
s.log(LogInformational, "called")
s.handlersMu.Lock()
if s.handlers != nil {
s.handlersMu.Unlock()
return
}
s.handlers = map[interface{}][]reflect.Value{}
s.handlersMu.Unlock()
s.AddHandler(s.onReady)
s.AddHandler(s.onResumed)
s.AddHandler(s.onVoiceServerUpdate)
s.AddHandler(s.onVoiceStateUpdate)
s.AddHandler(s.State.onInterface)
}
// onReady handles the ready event.
func (s *Session) onReady(se *Session, r *Ready) {
// Store the SessionID within the Session struct.
s.sessionID = r.SessionID
// Start the heartbeat to keep the connection alive.
go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval)
}
// onResumed handles the resumed event.
func (s *Session) onResumed(se *Session, r *Resumed) {
// Start the heartbeat to keep the connection alive.
go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval)
}

99
vendor/github.com/bwmarrin/discordgo/endpoints.go generated vendored Normal file
View File

@ -0,0 +1,99 @@
// Discordgo - Discord bindings for Go
// Available at https://github.com/bwmarrin/discordgo
// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains variables for all known Discord end points. All functions
// throughout the Discordgo package use these variables for all connections
// to Discord. These are all exported and you may modify them if needed.
package discordgo
// Known Discord API Endpoints.
var (
EndpointStatus = "https://status.discordapp.com/api/v2/"
EndpointSm = EndpointStatus + "scheduled-maintenances/"
EndpointSmActive = EndpointSm + "active.json"
EndpointSmUpcoming = EndpointSm + "upcoming.json"
EndpointDiscord = "https://discordapp.com/"
EndpointAPI = EndpointDiscord + "api/"
EndpointGuilds = EndpointAPI + "guilds/"
EndpointChannels = EndpointAPI + "channels/"
EndpointUsers = EndpointAPI + "users/"
EndpointGateway = EndpointAPI + "gateway"
EndpointAuth = EndpointAPI + "auth/"
EndpointLogin = EndpointAuth + "login"
EndpointLogout = EndpointAuth + "logout"
EndpointVerify = EndpointAuth + "verify"
EndpointVerifyResend = EndpointAuth + "verify/resend"
EndpointForgotPassword = EndpointAuth + "forgot"
EndpointResetPassword = EndpointAuth + "reset"
EndpointRegister = EndpointAuth + "register"
EndpointVoice = EndpointAPI + "/voice/"
EndpointVoiceRegions = EndpointVoice + "regions"
EndpointVoiceIce = EndpointVoice + "ice"
EndpointTutorial = EndpointAPI + "tutorial/"
EndpointTutorialIndicators = EndpointTutorial + "indicators"
EndpointTrack = EndpointAPI + "track"
EndpointSso = EndpointAPI + "sso"
EndpointReport = EndpointAPI + "report"
EndpointIntegrations = EndpointAPI + "integrations"
EndpointUser = func(uID string) string { return EndpointUsers + uID }
EndpointUserAvatar = func(uID, aID string) string { return EndpointUsers + uID + "/avatars/" + aID + ".jpg" }
EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" }
EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" }
EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID }
EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" }
EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" }
EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" }
EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" }
EndpointGuild = func(gID string) string { return EndpointGuilds + gID }
EndpointGuildInivtes = func(gID string) string { return EndpointGuilds + gID + "/invites" }
EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" }
EndpointGuildMembers = func(gID string) string { return EndpointGuilds + gID + "/members" }
EndpointGuildMember = func(gID, uID string) string { return EndpointGuilds + gID + "/members/" + uID }
EndpointGuildBans = func(gID string) string { return EndpointGuilds + gID + "/bans" }
EndpointGuildBan = func(gID, uID string) string { return EndpointGuilds + gID + "/bans/" + uID }
EndpointGuildIntegrations = func(gID string) string { return EndpointGuilds + gID + "/integrations" }
EndpointGuildIntegration = func(gID, iID string) string { return EndpointGuilds + gID + "/integrations/" + iID }
EndpointGuildIntegrationSync = func(gID, iID string) string { return EndpointGuilds + gID + "/integrations/" + iID + "/sync" }
EndpointGuildRoles = func(gID string) string { return EndpointGuilds + gID + "/roles" }
EndpointGuildRole = func(gID, rID string) string { return EndpointGuilds + gID + "/roles/" + rID }
EndpointGuildInvites = func(gID string) string { return EndpointGuilds + gID + "/invites" }
EndpointGuildEmbed = func(gID string) string { return EndpointGuilds + gID + "/embed" }
EndpointGuildPrune = func(gID string) string { return EndpointGuilds + gID + "/prune" }
EndpointGuildIcon = func(gID, hash string) string { return EndpointGuilds + gID + "/icons/" + hash + ".jpg" }
EndpointGuildSplash = func(gID, hash string) string { return EndpointGuilds + gID + "/splashes/" + hash + ".jpg" }
EndpointChannel = func(cID string) string { return EndpointChannels + cID }
EndpointChannelPermissions = func(cID string) string { return EndpointChannels + cID + "/permissions" }
EndpointChannelPermission = func(cID, tID string) string { return EndpointChannels + cID + "/permissions/" + tID }
EndpointChannelInvites = func(cID string) string { return EndpointChannels + cID + "/invites" }
EndpointChannelTyping = func(cID string) string { return EndpointChannels + cID + "/typing" }
EndpointChannelMessages = func(cID string) string { return EndpointChannels + cID + "/messages" }
EndpointChannelMessage = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID }
EndpointChannelMessageAck = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID + "/ack" }
EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk_delete" }
EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" }
EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID }
EndpointInvite = func(iID string) string { return EndpointAPI + "invite/" + iID }
EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" }
EndpointEmoji = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".png" }
EndpointOauth2 = EndpointAPI + "oauth2/"
EndpointApplications = EndpointOauth2 + "applications"
EndpointApplication = func(aID string) string { return EndpointApplications + "/" + aID }
EndpointApplicationsBot = func(aID string) string { return EndpointApplications + "/" + aID + "/bot" }
)

159
vendor/github.com/bwmarrin/discordgo/events.go generated vendored Normal file
View File

@ -0,0 +1,159 @@
package discordgo
// eventToInterface is a mapping of Discord WSAPI events to their
// DiscordGo event container.
// Each Discord WSAPI event maps to a unique interface.
// Use Session.AddHandler with one of these types to handle that
// type of event.
// eg:
// Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
// })
//
// or:
// Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) {
// })
var eventToInterface = map[string]interface{}{
"CHANNEL_CREATE": ChannelCreate{},
"CHANNEL_UPDATE": ChannelUpdate{},
"CHANNEL_DELETE": ChannelDelete{},
"GUILD_CREATE": GuildCreate{},
"GUILD_UPDATE": GuildUpdate{},
"GUILD_DELETE": GuildDelete{},
"GUILD_BAN_ADD": GuildBanAdd{},
"GUILD_BAN_REMOVE": GuildBanRemove{},
"GUILD_MEMBER_ADD": GuildMemberAdd{},
"GUILD_MEMBER_UPDATE": GuildMemberUpdate{},
"GUILD_MEMBER_REMOVE": GuildMemberRemove{},
"GUILD_ROLE_CREATE": GuildRoleCreate{},
"GUILD_ROLE_UPDATE": GuildRoleUpdate{},
"GUILD_ROLE_DELETE": GuildRoleDelete{},
"GUILD_INTEGRATIONS_UPDATE": GuildIntegrationsUpdate{},
"GUILD_EMOJIS_UPDATE": GuildEmojisUpdate{},
"MESSAGE_ACK": MessageAck{},
"MESSAGE_CREATE": MessageCreate{},
"MESSAGE_UPDATE": MessageUpdate{},
"MESSAGE_DELETE": MessageDelete{},
"PRESENCE_UPDATE": PresenceUpdate{},
"PRESENCES_REPLACE": PresencesReplace{},
"READY": Ready{},
"USER_UPDATE": UserUpdate{},
"USER_SETTINGS_UPDATE": UserSettingsUpdate{},
"USER_GUILD_SETTINGS_UPDATE": UserGuildSettingsUpdate{},
"TYPING_START": TypingStart{},
"VOICE_SERVER_UPDATE": VoiceServerUpdate{},
"VOICE_STATE_UPDATE": VoiceStateUpdate{},
"RESUMED": Resumed{},
}
// Connect is an empty struct for an event.
type Connect struct{}
// Disconnect is an empty struct for an event.
type Disconnect struct{}
// RateLimit is a struct for the RateLimited event
type RateLimit struct {
*TooManyRequests
URL string
}
// MessageCreate is a wrapper struct for an event.
type MessageCreate struct {
*Message
}
// MessageUpdate is a wrapper struct for an event.
type MessageUpdate struct {
*Message
}
// MessageDelete is a wrapper struct for an event.
type MessageDelete struct {
*Message
}
// ChannelCreate is a wrapper struct for an event.
type ChannelCreate struct {
*Channel
}
// ChannelUpdate is a wrapper struct for an event.
type ChannelUpdate struct {
*Channel
}
// ChannelDelete is a wrapper struct for an event.
type ChannelDelete struct {
*Channel
}
// GuildCreate is a wrapper struct for an event.
type GuildCreate struct {
*Guild
}
// GuildUpdate is a wrapper struct for an event.
type GuildUpdate struct {
*Guild
}
// GuildDelete is a wrapper struct for an event.
type GuildDelete struct {
*Guild
}
// GuildBanAdd is a wrapper struct for an event.
type GuildBanAdd struct {
*GuildBan
}
// GuildBanRemove is a wrapper struct for an event.
type GuildBanRemove struct {
*GuildBan
}
// GuildMemberAdd is a wrapper struct for an event.
type GuildMemberAdd struct {
*Member
}
// GuildMemberUpdate is a wrapper struct for an event.
type GuildMemberUpdate struct {
*Member
}
// GuildMemberRemove is a wrapper struct for an event.
type GuildMemberRemove struct {
*Member
}
// GuildRoleCreate is a wrapper struct for an event.
type GuildRoleCreate struct {
*GuildRole
}
// GuildRoleUpdate is a wrapper struct for an event.
type GuildRoleUpdate struct {
*GuildRole
}
// PresencesReplace is an array of Presences for an event.
type PresencesReplace []*Presence
// VoiceStateUpdate is a wrapper struct for an event.
type VoiceStateUpdate struct {
*VoiceState
}
// UserUpdate is a wrapper struct for an event.
type UserUpdate struct {
*User
}
// UserSettingsUpdate is a map for an event.
type UserSettingsUpdate map[string]interface{}
// UserGuildSettingsUpdate is a map for an event.
type UserGuildSettingsUpdate struct {
*UserGuildSettings
}

View File

@ -0,0 +1,186 @@
package main
import (
"encoding/binary"
"flag"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/bwmarrin/discordgo"
)
func init() {
flag.StringVar(&token, "t", "", "Account Token")
flag.Parse()
}
var token string
var buffer = make([][]byte, 0)
func main() {
if token == "" {
fmt.Println("No token provided. Please run: airhorn -t <bot token>")
return
}
// Load the sound file.
err := loadSound()
if err != nil {
fmt.Println("Error loading sound: ", err)
fmt.Println("Please copy $GOPATH/src/github.com/bwmarrin/examples/airhorn/airhorn.dca to this directory.")
return
}
// Create a new Discord session using the provided token.
dg, err := discordgo.New(token)
if err != nil {
fmt.Println("Error creating Discord session: ", err)
return
}
// Register ready as a callback for the ready events.
dg.AddHandler(ready)
// Register messageCreate as a callback for the messageCreate events.
dg.AddHandler(messageCreate)
// Register guildCreate as a callback for the guildCreate events.
dg.AddHandler(guildCreate)
// Open the websocket and begin listening.
err = dg.Open()
if err != nil {
fmt.Println("Error opening Discord session: ", err)
}
fmt.Println("Airhorn is now running. Press CTRL-C to exit.")
// Simple way to keep program running until CTRL-C is pressed.
<-make(chan struct{})
return
}
func ready(s *discordgo.Session, event *discordgo.Ready) {
// Set the playing status.
_ = s.UpdateStatus(0, "!airhorn")
}
// This function will be called (due to AddHandler above) every time a new
// message is created on any channel that the autenticated bot has access to.
func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
if strings.HasPrefix(m.Content, "!airhorn") {
// Find the channel that the message came from.
c, err := s.State.Channel(m.ChannelID)
if err != nil {
// Could not find channel.
return
}
// Find the guild for that channel.
g, err := s.State.Guild(c.GuildID)
if err != nil {
// Could not find guild.
return
}
// Look for the message sender in that guilds current voice states.
for _, vs := range g.VoiceStates {
if vs.UserID == m.Author.ID {
err = playSound(s, g.ID, vs.ChannelID)
if err != nil {
fmt.Println("Error playing sound:", err)
}
return
}
}
}
}
// This function will be called (due to AddHandler above) every time a new
// guild is joined.
func guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) {
if event.Guild.Unavailable != nil {
return
}
for _, channel := range event.Guild.Channels {
if channel.ID == event.Guild.ID {
_, _ = s.ChannelMessageSend(channel.ID, "Airhorn is ready! Type !airhorn while in a voice channel to play a sound.")
return
}
}
}
// loadSound attempts to load an encoded sound file from disk.
func loadSound() error {
file, err := os.Open("airhorn.dca")
if err != nil {
fmt.Println("Error opening dca file :", err)
return err
}
var opuslen int16
for {
// Read opus frame length from dca file.
err = binary.Read(file, binary.LittleEndian, &opuslen)
// If this is the end of the file, just return.
if err == io.EOF || err == io.ErrUnexpectedEOF {
return nil
}
if err != nil {
fmt.Println("Error reading from dca file :", err)
return err
}
// Read encoded pcm from dca file.
InBuf := make([]byte, opuslen)
err = binary.Read(file, binary.LittleEndian, &InBuf)
// Should not be any end of file errors
if err != nil {
fmt.Println("Error reading from dca file :", err)
return err
}
// Append encoded pcm data to the buffer.
buffer = append(buffer, InBuf)
}
}
// playSound plays the current buffer to the provided channel.
func playSound(s *discordgo.Session, guildID, channelID string) (err error) {
// Join the provided voice channel.
vc, err := s.ChannelVoiceJoin(guildID, channelID, false, true)
if err != nil {
return err
}
// Sleep for a specified amount of time before playing the sound
time.Sleep(250 * time.Millisecond)
// Start speaking.
_ = vc.Speaking(true)
// Send the buffer data.
for _, buff := range buffer {
vc.OpusSend <- buff
}
// Stop speaking
_ = vc.Speaking(false)
// Sleep for a specificed amount of time before ending.
time.Sleep(250 * time.Millisecond)
// Disconnect from the provided voice channel.
_ = vc.Disconnect()
return nil
}

View File

@ -0,0 +1,98 @@
package main
import (
"flag"
"fmt"
"github.com/bwmarrin/discordgo"
)
// Variables used for command line options
var (
Email string
Password string
Token string
AppName string
DeleteID string
ListOnly bool
)
func init() {
flag.StringVar(&Email, "e", "", "Account Email")
flag.StringVar(&Password, "p", "", "Account Password")
flag.StringVar(&Token, "t", "", "Account Token")
flag.StringVar(&DeleteID, "d", "", "Application ID to delete")
flag.BoolVar(&ListOnly, "l", false, "List Applications Only")
flag.StringVar(&AppName, "a", "", "App/Bot Name")
flag.Parse()
}
func main() {
var err error
// Create a new Discord session using the provided login information.
dg, err := discordgo.New(Email, Password, Token)
if err != nil {
fmt.Println("error creating Discord session,", err)
return
}
// If -l set, only display a list of existing applications
// for the given account.
if ListOnly {
aps, err2 := dg.Applications()
if err2 != nil {
fmt.Println("error fetching applications,", err)
return
}
for k, v := range aps {
fmt.Printf("%d : --------------------------------------\n", k)
fmt.Printf("ID: %s\n", v.ID)
fmt.Printf("Name: %s\n", v.Name)
fmt.Printf("Secret: %s\n", v.Secret)
fmt.Printf("Description: %s\n", v.Description)
}
return
}
// if -d set, delete the given Application
if DeleteID != "" {
err = dg.ApplicationDelete(DeleteID)
if err != nil {
fmt.Println("error deleting application,", err)
}
return
}
// Create a new application.
ap := &discordgo.Application{}
ap.Name = AppName
ap, err = dg.ApplicationCreate(ap)
if err != nil {
fmt.Println("error creating new applicaiton,", err)
return
}
fmt.Printf("Application created successfully:\n")
fmt.Printf("ID: %s\n", ap.ID)
fmt.Printf("Name: %s\n", ap.Name)
fmt.Printf("Secret: %s\n\n", ap.Secret)
// Create the bot account under the application we just created
bot, err := dg.ApplicationBotCreate(ap.ID)
if err != nil {
fmt.Println("error creating bot account,", err)
return
}
fmt.Printf("Bot account created successfully.\n")
fmt.Printf("ID: %s\n", bot.ID)
fmt.Printf("Username: %s\n", bot.Username)
fmt.Printf("Token: %s\n\n", bot.Token)
fmt.Println("Please save the above posted info in a secure place.")
fmt.Println("You will need that information to login with your bot account.")
return
}

View File

@ -0,0 +1,73 @@
package main
import (
"encoding/base64"
"flag"
"fmt"
"io/ioutil"
"net/http"
"github.com/bwmarrin/discordgo"
)
// Variables used for command line parameters
var (
Email string
Password string
Token string
Avatar string
BotID string
BotUsername string
)
func init() {
flag.StringVar(&Email, "e", "", "Account Email")
flag.StringVar(&Password, "p", "", "Account Password")
flag.StringVar(&Token, "t", "", "Account Token")
flag.StringVar(&Avatar, "f", "./avatar.jpg", "Avatar File Name")
flag.Parse()
}
func main() {
// Create a new Discord session using the provided login information.
// Use discordgo.New(Token) to just use a token for login.
dg, err := discordgo.New(Email, Password, Token)
if err != nil {
fmt.Println("error creating Discord session,", err)
return
}
bot, err := dg.User("@me")
if err != nil {
fmt.Println("error fetching the bot details,", err)
return
}
BotID = bot.ID
BotUsername = bot.Username
changeAvatar(dg)
fmt.Println("Bot is now running. Press CTRL-C to exit.")
// Simple way to keep program running until CTRL-C is pressed.
<-make(chan struct{})
return
}
// Helper function to change the avatar
func changeAvatar(s *discordgo.Session) {
img, err := ioutil.ReadFile(Avatar)
if err != nil {
fmt.Println(err)
}
base64 := base64.StdEncoding.EncodeToString(img)
avatar := fmt.Sprintf("data:%s;base64,%s", http.DetectContentType(img), base64)
_, err = s.UserUpdate("", "", BotUsername, avatar, "")
if err != nil {
fmt.Println(err)
}
}

View File

@ -0,0 +1,86 @@
package main
import (
"encoding/base64"
"flag"
"fmt"
"io/ioutil"
"net/http"
"github.com/bwmarrin/discordgo"
)
// Variables used for command line parameters
var (
Email string
Password string
Token string
URL string
BotID string
BotUsername string
)
func init() {
flag.StringVar(&Email, "e", "", "Account Email")
flag.StringVar(&Password, "p", "", "Account Password")
flag.StringVar(&Token, "t", "", "Account Token")
flag.StringVar(&URL, "l", "http://bwmarrin.github.io/discordgo/img/discordgo.png", "Link to the avatar image")
flag.Parse()
}
func main() {
// Create a new Discord session using the provided login information.
// Use discordgo.New(Token) to just use a token for login.
dg, err := discordgo.New(Email, Password, Token)
if err != nil {
fmt.Println("error creating Discord session,", err)
return
}
bot, err := dg.User("@me")
if err != nil {
fmt.Println("error fetching the bot details,", err)
return
}
BotID = bot.ID
BotUsername = bot.Username
changeAvatar(dg)
fmt.Println("Bot is now running. Press CTRL-C to exit.")
// Simple way to keep program running until CTRL-C is pressed.
<-make(chan struct{})
return
}
// Helper function to change the avatar
func changeAvatar(s *discordgo.Session) {
resp, err := http.Get(URL)
if err != nil {
fmt.Println("Error retrieving the file, ", err)
return
}
defer func() {
_ = resp.Body.Close()
}()
img, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading the response, ", err)
return
}
base64 := base64.StdEncoding.EncodeToString(img)
avatar := fmt.Sprintf("data:%s;base64,%s", http.DetectContentType(img), base64)
_, err = s.UserUpdate("", "", BotUsername, avatar, "")
if err != nil {
fmt.Println("Error setting the avatar, ", err)
}
}

View File

@ -0,0 +1,33 @@
package main
import (
"flag"
"fmt"
"github.com/bwmarrin/discordgo"
)
// Variables used for command line parameters
var (
Email string
Password string
)
func init() {
flag.StringVar(&Email, "e", "", "Account Email")
flag.StringVar(&Password, "p", "", "Account Password")
flag.Parse()
}
func main() {
// Create a new Discord session using the provided login information.
dg, err := discordgo.New(Email, Password)
if err != nil {
fmt.Println("error creating Discord session,", err)
return
}
fmt.Printf("Your Authentication Token is:\n\n%s\n", dg.Token)
}

View File

@ -0,0 +1,58 @@
package main
import (
"flag"
"fmt"
"time"
"github.com/bwmarrin/discordgo"
)
// Variables used for command line parameters
var (
Email string
Password string
Token string
)
func init() {
flag.StringVar(&Email, "e", "", "Account Email")
flag.StringVar(&Password, "p", "", "Account Password")
flag.StringVar(&Token, "t", "", "Account Token")
flag.Parse()
}
func main() {
// Create a new Discord session using the provided login information.
// Use discordgo.New(Token) to just use a token for login.
dg, err := discordgo.New(Email, Password, Token)
if err != nil {
fmt.Println("error creating Discord session,", err)
return
}
// Register messageCreate as a callback for the messageCreate events.
dg.AddHandler(messageCreate)
// Open the websocket and begin listening.
err = dg.Open()
if err != nil {
fmt.Println("error opening connection,", err)
return
}
fmt.Println("Bot is now running. Press CTRL-C to exit.")
// Simple way to keep program running until CTRL-C is pressed.
<-make(chan struct{})
return
}
// This function will be called (due to AddHandler above) every time a new
// message is created on any channel that the autenticated bot has access to.
func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
// Print message to stdout.
fmt.Printf("%20s %20s %20s > %s\n", m.ChannelID, time.Now().Format(time.Stamp), m.Author.Username, m.Content)
}

View File

@ -0,0 +1,78 @@
package main
import (
"flag"
"fmt"
"github.com/bwmarrin/discordgo"
)
// Variables used for command line parameters
var (
Email string
Password string
Token string
BotID string
)
func init() {
flag.StringVar(&Email, "e", "", "Account Email")
flag.StringVar(&Password, "p", "", "Account Password")
flag.StringVar(&Token, "t", "", "Account Token")
flag.Parse()
}
func main() {
// Create a new Discord session using the provided login information.
dg, err := discordgo.New(Email, Password, Token)
if err != nil {
fmt.Println("error creating Discord session,", err)
return
}
// Get the account information.
u, err := dg.User("@me")
if err != nil {
fmt.Println("error obtaining account details,", err)
}
// Store the account ID for later use.
BotID = u.ID
// Register messageCreate as a callback for the messageCreate events.
dg.AddHandler(messageCreate)
// Open the websocket and begin listening.
err = dg.Open()
if err != nil {
fmt.Println("error opening connection,", err)
return
}
fmt.Println("Bot is now running. Press CTRL-C to exit.")
// Simple way to keep program running until CTRL-C is pressed.
<-make(chan struct{})
return
}
// This function will be called (due to AddHandler above) every time a new
// message is created on any channel that the autenticated bot has access to.
func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore all messages created by the bot itself
if m.Author.ID == BotID {
return
}
// If the message is "ping" reply with "Pong!"
if m.Content == "ping" {
_, _ = s.ChannelMessageSend(m.ChannelID, "Pong!")
}
// If the message is "pong" reply with "Ping!"
if m.Content == "pong" {
_, _ = s.ChannelMessageSend(m.ChannelID, "Ping!")
}
}

95
vendor/github.com/bwmarrin/discordgo/logging.go generated vendored Normal file
View File

@ -0,0 +1,95 @@
// Discordgo - Discord bindings for Go
// Available at https://github.com/bwmarrin/discordgo
// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains code related to discordgo package logging
package discordgo
import (
"fmt"
"log"
"runtime"
"strings"
)
const (
// LogError level is used for critical errors that could lead to data loss
// or panic that would not be returned to a calling function.
LogError int = iota
// LogWarning level is used for very abnormal events and errors that are
// also returend to a calling function.
LogWarning
// LogInformational level is used for normal non-error activity
LogInformational
// LogDebug level is for very detailed non-error activity. This is
// very spammy and will impact performance.
LogDebug
)
// msglog provides package wide logging consistancy for discordgo
// the format, a... portion this command follows that of fmt.Printf
// msgL : LogLevel of the message
// caller : 1 + the number of callers away from the message source
// format : Printf style message format
// a ... : comma seperated list of values to pass
func msglog(msgL, caller int, format string, a ...interface{}) {
pc, file, line, _ := runtime.Caller(caller)
files := strings.Split(file, "/")
file = files[len(files)-1]
name := runtime.FuncForPC(pc).Name()
fns := strings.Split(name, ".")
name = fns[len(fns)-1]
msg := fmt.Sprintf(format, a...)
log.Printf("[DG%d] %s:%d:%s() %s\n", msgL, file, line, name, msg)
}
// helper function that wraps msglog for the Session struct
// This adds a check to insure the message is only logged
// if the session log level is equal or higher than the
// message log level
func (s *Session) log(msgL int, format string, a ...interface{}) {
if msgL > s.LogLevel {
return
}
msglog(msgL, 2, format, a...)
}
// helper function that wraps msglog for the VoiceConnection struct
// This adds a check to insure the message is only logged
// if the voice connection log level is equal or higher than the
// message log level
func (v *VoiceConnection) log(msgL int, format string, a ...interface{}) {
if msgL > v.LogLevel {
return
}
msglog(msgL, 2, format, a...)
}
// printJSON is a helper function to display JSON data in a easy to read format.
/* NOT USED ATM
func printJSON(body []byte) {
var prettyJSON bytes.Buffer
error := json.Indent(&prettyJSON, body, "", "\t")
if error != nil {
log.Print("JSON parse error: ", error)
}
log.Println(string(prettyJSON.Bytes()))
}
*/

82
vendor/github.com/bwmarrin/discordgo/message.go generated vendored Normal file
View File

@ -0,0 +1,82 @@
// Discordgo - Discord bindings for Go
// Available at https://github.com/bwmarrin/discordgo
// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains code related to the Message struct
package discordgo
import (
"fmt"
"regexp"
)
// A Message stores all data related to a specific Discord message.
type Message struct {
ID string `json:"id"`
ChannelID string `json:"channel_id"`
Content string `json:"content"`
Timestamp string `json:"timestamp"`
EditedTimestamp string `json:"edited_timestamp"`
MentionRoles []string `json:"mention_roles"`
Tts bool `json:"tts"`
MentionEveryone bool `json:"mention_everyone"`
Author *User `json:"author"`
Attachments []*MessageAttachment `json:"attachments"`
Embeds []*MessageEmbed `json:"embeds"`
Mentions []*User `json:"mentions"`
}
// A MessageAttachment stores data for message attachments.
type MessageAttachment struct {
ID string `json:"id"`
URL string `json:"url"`
ProxyURL string `json:"proxy_url"`
Filename string `json:"filename"`
Width int `json:"width"`
Height int `json:"height"`
Size int `json:"size"`
}
// An MessageEmbed stores data for message embeds.
type MessageEmbed struct {
URL string `json:"url"`
Type string `json:"type"`
Title string `json:"title"`
Description string `json:"description"`
Thumbnail *struct {
URL string `json:"url"`
ProxyURL string `json:"proxy_url"`
Width int `json:"width"`
Height int `json:"height"`
} `json:"thumbnail"`
Provider *struct {
URL string `json:"url"`
Name string `json:"name"`
} `json:"provider"`
Author *struct {
URL string `json:"url"`
Name string `json:"name"`
} `json:"author"`
Video *struct {
URL string `json:"url"`
Width int `json:"width"`
Height int `json:"height"`
} `json:"video"`
}
// ContentWithMentionsReplaced will replace all @<id> mentions with the
// username of the mention.
func (m *Message) ContentWithMentionsReplaced() string {
if m.Mentions == nil {
return m.Content
}
content := m.Content
for _, user := range m.Mentions {
content = regexp.MustCompile(fmt.Sprintf("<@!?(%s)>", user.ID)).ReplaceAllString(content, "@"+user.Username)
}
return content
}

120
vendor/github.com/bwmarrin/discordgo/oauth2.go generated vendored Normal file
View File

@ -0,0 +1,120 @@
// Discordgo - Discord bindings for Go
// Available at https://github.com/bwmarrin/discordgo
// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains functions related to Discord OAuth2 endpoints
package discordgo
// ------------------------------------------------------------------------------------------------
// Code specific to Discord OAuth2 Applications
// ------------------------------------------------------------------------------------------------
// An Application struct stores values for a Discord OAuth2 Application
type Application struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Icon string `json:"icon,omitempty"`
Secret string `json:"secret,omitempty"`
RedirectURIs *[]string `json:"redirect_uris,omitempty"`
}
// Application returns an Application structure of a specific Application
// appID : The ID of an Application
func (s *Session) Application(appID string) (st *Application, err error) {
body, err := s.Request("GET", EndpointApplication(appID), nil)
if err != nil {
return
}
err = unmarshal(body, &st)
return
}
// Applications returns all applications for the authenticated user
func (s *Session) Applications() (st []*Application, err error) {
body, err := s.Request("GET", EndpointApplications, nil)
if err != nil {
return
}
err = unmarshal(body, &st)
return
}
// ApplicationCreate creates a new Application
// name : Name of Application / Bot
// uris : Redirect URIs (Not required)
func (s *Session) ApplicationCreate(ap *Application) (st *Application, err error) {
data := struct {
Name string `json:"name"`
Description string `json:"description"`
RedirectURIs *[]string `json:"redirect_uris,omitempty"`
}{ap.Name, ap.Description, ap.RedirectURIs}
body, err := s.Request("POST", EndpointApplications, data)
if err != nil {
return
}
err = unmarshal(body, &st)
return
}
// ApplicationUpdate updates an existing Application
// var : desc
func (s *Session) ApplicationUpdate(appID string, ap *Application) (st *Application, err error) {
data := struct {
Name string `json:"name"`
Description string `json:"description"`
RedirectURIs *[]string `json:"redirect_uris,omitempty"`
}{ap.Name, ap.Description, ap.RedirectURIs}
body, err := s.Request("PUT", EndpointApplication(appID), data)
if err != nil {
return
}
err = unmarshal(body, &st)
return
}
// ApplicationDelete deletes an existing Application
// appID : The ID of an Application
func (s *Session) ApplicationDelete(appID string) (err error) {
_, err = s.Request("DELETE", EndpointApplication(appID), nil)
if err != nil {
return
}
return
}
// ------------------------------------------------------------------------------------------------
// Code specific to Discord OAuth2 Application Bots
// ------------------------------------------------------------------------------------------------
// ApplicationBotCreate creates an Application Bot Account
//
// appID : The ID of an Application
//
// NOTE: func name may change, if I can think up something better.
func (s *Session) ApplicationBotCreate(appID string) (st *User, err error) {
body, err := s.Request("POST", EndpointApplicationsBot(appID), nil)
if err != nil {
return
}
err = unmarshal(body, &st)
return
}

1403
vendor/github.com/bwmarrin/discordgo/restapi.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

746
vendor/github.com/bwmarrin/discordgo/state.go generated vendored Normal file
View File

@ -0,0 +1,746 @@
// Discordgo - Discord bindings for Go
// Available at https://github.com/bwmarrin/discordgo
// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains code related to state tracking. If enabled, state
// tracking will capture the initial READY packet and many other websocket
// events and maintain an in-memory state of of guilds, channels, users, and
// so forth. This information can be accessed through the Session.State struct.
package discordgo
import (
"errors"
"sync"
)
// ErrNilState is returned when the state is nil.
var ErrNilState = errors.New("State not instantiated, please use discordgo.New() or assign Session.State.")
// A State contains the current known state.
// As discord sends this in a READY blob, it seems reasonable to simply
// use that struct as the data store.
type State struct {
sync.RWMutex
Ready
MaxMessageCount int
TrackChannels bool
TrackEmojis bool
TrackMembers bool
TrackRoles bool
TrackVoice bool
guildMap map[string]*Guild
channelMap map[string]*Channel
}
// NewState creates an empty state.
func NewState() *State {
return &State{
Ready: Ready{
PrivateChannels: []*Channel{},
Guilds: []*Guild{},
},
TrackChannels: true,
TrackEmojis: true,
TrackMembers: true,
TrackRoles: true,
TrackVoice: true,
guildMap: make(map[string]*Guild),
channelMap: make(map[string]*Channel),
}
}
// OnReady takes a Ready event and updates all internal state.
func (s *State) OnReady(r *Ready) error {
if s == nil {
return ErrNilState
}
s.Lock()
defer s.Unlock()
s.Ready = *r
for _, g := range s.Guilds {
s.guildMap[g.ID] = g
for _, c := range g.Channels {
c.GuildID = g.ID
s.channelMap[c.ID] = c
}
}
for _, c := range s.PrivateChannels {
s.channelMap[c.ID] = c
}
return nil
}
// GuildAdd adds a guild to the current world state, or
// updates it if it already exists.
func (s *State) GuildAdd(guild *Guild) error {
if s == nil {
return ErrNilState
}
s.Lock()
defer s.Unlock()
// Update the channels to point to the right guild, adding them to the channelMap as we go
for _, c := range guild.Channels {
c.GuildID = guild.ID
s.channelMap[c.ID] = c
}
// If the guild exists, replace it.
if g, ok := s.guildMap[guild.ID]; ok {
// If this guild already exists with data, don't stomp on props.
if g.Unavailable != nil && !*g.Unavailable {
guild.Members = g.Members
guild.Presences = g.Presences
guild.Channels = g.Channels
guild.VoiceStates = g.VoiceStates
}
*g = *guild
return nil
}
s.Guilds = append(s.Guilds, guild)
s.guildMap[guild.ID] = guild
return nil
}
// GuildRemove removes a guild from current world state.
func (s *State) GuildRemove(guild *Guild) error {
if s == nil {
return ErrNilState
}
_, err := s.Guild(guild.ID)
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
delete(s.guildMap, guild.ID)
for i, g := range s.Guilds {
if g.ID == guild.ID {
s.Guilds = append(s.Guilds[:i], s.Guilds[i+1:]...)
return nil
}
}
return nil
}
// Guild gets a guild by ID.
// Useful for querying if @me is in a guild:
// _, err := discordgo.Session.State.Guild(guildID)
// isInGuild := err == nil
func (s *State) Guild(guildID string) (*Guild, error) {
if s == nil {
return nil, ErrNilState
}
s.RLock()
defer s.RUnlock()
if g, ok := s.guildMap[guildID]; ok {
return g, nil
}
return nil, errors.New("Guild not found.")
}
// TODO: Consider moving Guild state update methods onto *Guild.
// MemberAdd adds a member to the current world state, or
// updates it if it already exists.
func (s *State) MemberAdd(member *Member) error {
if s == nil {
return ErrNilState
}
guild, err := s.Guild(member.GuildID)
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
for i, m := range guild.Members {
if m.User.ID == member.User.ID {
guild.Members[i] = member
return nil
}
}
guild.Members = append(guild.Members, member)
return nil
}
// MemberRemove removes a member from current world state.
func (s *State) MemberRemove(member *Member) error {
if s == nil {
return ErrNilState
}
guild, err := s.Guild(member.GuildID)
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
for i, m := range guild.Members {
if m.User.ID == member.User.ID {
guild.Members = append(guild.Members[:i], guild.Members[i+1:]...)
return nil
}
}
return errors.New("Member not found.")
}
// Member gets a member by ID from a guild.
func (s *State) Member(guildID, userID string) (*Member, error) {
if s == nil {
return nil, ErrNilState
}
guild, err := s.Guild(guildID)
if err != nil {
return nil, err
}
s.RLock()
defer s.RUnlock()
for _, m := range guild.Members {
if m.User.ID == userID {
return m, nil
}
}
return nil, errors.New("Member not found.")
}
// RoleAdd adds a role to the current world state, or
// updates it if it already exists.
func (s *State) RoleAdd(guildID string, role *Role) error {
if s == nil {
return ErrNilState
}
guild, err := s.Guild(guildID)
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
for i, r := range guild.Roles {
if r.ID == role.ID {
guild.Roles[i] = role
return nil
}
}
guild.Roles = append(guild.Roles, role)
return nil
}
// RoleRemove removes a role from current world state by ID.
func (s *State) RoleRemove(guildID, roleID string) error {
if s == nil {
return ErrNilState
}
guild, err := s.Guild(guildID)
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
for i, r := range guild.Roles {
if r.ID == roleID {
guild.Roles = append(guild.Roles[:i], guild.Roles[i+1:]...)
return nil
}
}
return errors.New("Role not found.")
}
// Role gets a role by ID from a guild.
func (s *State) Role(guildID, roleID string) (*Role, error) {
if s == nil {
return nil, ErrNilState
}
guild, err := s.Guild(guildID)
if err != nil {
return nil, err
}
s.RLock()
defer s.RUnlock()
for _, r := range guild.Roles {
if r.ID == roleID {
return r, nil
}
}
return nil, errors.New("Role not found.")
}
// ChannelAdd adds a guild to the current world state, or
// updates it if it already exists.
// Channels may exist either as PrivateChannels or inside
// a guild.
func (s *State) ChannelAdd(channel *Channel) error {
if s == nil {
return ErrNilState
}
s.Lock()
defer s.Unlock()
// If the channel exists, replace it
if c, ok := s.channelMap[channel.ID]; ok {
channel.Messages = c.Messages
channel.PermissionOverwrites = c.PermissionOverwrites
*c = *channel
return nil
}
if channel.IsPrivate {
s.PrivateChannels = append(s.PrivateChannels, channel)
} else {
guild, ok := s.guildMap[channel.GuildID]
if !ok {
return errors.New("Guild for channel not found.")
}
guild.Channels = append(guild.Channels, channel)
}
s.channelMap[channel.ID] = channel
return nil
}
// ChannelRemove removes a channel from current world state.
func (s *State) ChannelRemove(channel *Channel) error {
if s == nil {
return ErrNilState
}
_, err := s.Channel(channel.ID)
if err != nil {
return err
}
if channel.IsPrivate {
s.Lock()
defer s.Unlock()
for i, c := range s.PrivateChannels {
if c.ID == channel.ID {
s.PrivateChannels = append(s.PrivateChannels[:i], s.PrivateChannels[i+1:]...)
break
}
}
} else {
guild, err := s.Guild(channel.GuildID)
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
for i, c := range guild.Channels {
if c.ID == channel.ID {
guild.Channels = append(guild.Channels[:i], guild.Channels[i+1:]...)
break
}
}
}
delete(s.channelMap, channel.ID)
return nil
}
// GuildChannel gets a channel by ID from a guild.
// This method is Deprecated, use Channel(channelID)
func (s *State) GuildChannel(guildID, channelID string) (*Channel, error) {
return s.Channel(channelID)
}
// PrivateChannel gets a private channel by ID.
// This method is Deprecated, use Channel(channelID)
func (s *State) PrivateChannel(channelID string) (*Channel, error) {
return s.Channel(channelID)
}
// Channel gets a channel by ID, it will look in all guilds an private channels.
func (s *State) Channel(channelID string) (*Channel, error) {
if s == nil {
return nil, ErrNilState
}
s.RLock()
defer s.RUnlock()
if c, ok := s.channelMap[channelID]; ok {
return c, nil
}
return nil, errors.New("Channel not found.")
}
// Emoji returns an emoji for a guild and emoji id.
func (s *State) Emoji(guildID, emojiID string) (*Emoji, error) {
if s == nil {
return nil, ErrNilState
}
guild, err := s.Guild(guildID)
if err != nil {
return nil, err
}
s.RLock()
defer s.RUnlock()
for _, e := range guild.Emojis {
if e.ID == emojiID {
return e, nil
}
}
return nil, errors.New("Emoji not found.")
}
// EmojiAdd adds an emoji to the current world state.
func (s *State) EmojiAdd(guildID string, emoji *Emoji) error {
if s == nil {
return ErrNilState
}
guild, err := s.Guild(guildID)
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
for i, e := range guild.Emojis {
if e.ID == emoji.ID {
guild.Emojis[i] = emoji
return nil
}
}
guild.Emojis = append(guild.Emojis, emoji)
return nil
}
// EmojisAdd adds multiple emojis to the world state.
func (s *State) EmojisAdd(guildID string, emojis []*Emoji) error {
for _, e := range emojis {
if err := s.EmojiAdd(guildID, e); err != nil {
return err
}
}
return nil
}
// MessageAdd adds a message to the current world state, or updates it if it exists.
// If the channel cannot be found, the message is discarded.
// Messages are kept in state up to s.MaxMessageCount
func (s *State) MessageAdd(message *Message) error {
if s == nil {
return ErrNilState
}
c, err := s.Channel(message.ChannelID)
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
// If the message exists, merge in the new message contents.
for _, m := range c.Messages {
if m.ID == message.ID {
if message.Content != "" {
m.Content = message.Content
}
if message.EditedTimestamp != "" {
m.EditedTimestamp = message.EditedTimestamp
}
if message.Mentions != nil {
m.Mentions = message.Mentions
}
if message.Embeds != nil {
m.Embeds = message.Embeds
}
if message.Attachments != nil {
m.Attachments = message.Attachments
}
return nil
}
}
c.Messages = append(c.Messages, message)
if len(c.Messages) > s.MaxMessageCount {
c.Messages = c.Messages[len(c.Messages)-s.MaxMessageCount:]
}
return nil
}
// MessageRemove removes a message from the world state.
func (s *State) MessageRemove(message *Message) error {
if s == nil {
return ErrNilState
}
c, err := s.Channel(message.ChannelID)
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
for i, m := range c.Messages {
if m.ID == message.ID {
c.Messages = append(c.Messages[:i], c.Messages[i+1:]...)
return nil
}
}
return errors.New("Message not found.")
}
func (s *State) voiceStateUpdate(update *VoiceStateUpdate) error {
guild, err := s.Guild(update.GuildID)
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
// Handle Leaving Channel
if update.ChannelID == "" {
for i, state := range guild.VoiceStates {
if state.UserID == update.UserID {
guild.VoiceStates = append(guild.VoiceStates[:i], guild.VoiceStates[i+1:]...)
return nil
}
}
} else {
for i, state := range guild.VoiceStates {
if state.UserID == update.UserID {
guild.VoiceStates[i] = update.VoiceState
return nil
}
}
guild.VoiceStates = append(guild.VoiceStates, update.VoiceState)
}
return nil
}
// Message gets a message by channel and message ID.
func (s *State) Message(channelID, messageID string) (*Message, error) {
if s == nil {
return nil, ErrNilState
}
c, err := s.Channel(channelID)
if err != nil {
return nil, err
}
s.RLock()
defer s.RUnlock()
for _, m := range c.Messages {
if m.ID == messageID {
return m, nil
}
}
return nil, errors.New("Message not found.")
}
// onInterface handles all events related to states.
func (s *State) onInterface(se *Session, i interface{}) (err error) {
if s == nil {
return ErrNilState
}
if !se.StateEnabled {
return nil
}
switch t := i.(type) {
case *Ready:
err = s.OnReady(t)
case *GuildCreate:
err = s.GuildAdd(t.Guild)
case *GuildUpdate:
err = s.GuildAdd(t.Guild)
case *GuildDelete:
err = s.GuildRemove(t.Guild)
case *GuildMemberAdd:
if s.TrackMembers {
err = s.MemberAdd(t.Member)
}
case *GuildMemberUpdate:
if s.TrackMembers {
err = s.MemberAdd(t.Member)
}
case *GuildMemberRemove:
if s.TrackMembers {
err = s.MemberRemove(t.Member)
}
case *GuildRoleCreate:
if s.TrackRoles {
err = s.RoleAdd(t.GuildID, t.Role)
}
case *GuildRoleUpdate:
if s.TrackRoles {
err = s.RoleAdd(t.GuildID, t.Role)
}
case *GuildRoleDelete:
if s.TrackRoles {
err = s.RoleRemove(t.GuildID, t.RoleID)
}
case *GuildEmojisUpdate:
if s.TrackEmojis {
err = s.EmojisAdd(t.GuildID, t.Emojis)
}
case *ChannelCreate:
if s.TrackChannels {
err = s.ChannelAdd(t.Channel)
}
case *ChannelUpdate:
if s.TrackChannels {
err = s.ChannelAdd(t.Channel)
}
case *ChannelDelete:
if s.TrackChannels {
err = s.ChannelRemove(t.Channel)
}
case *MessageCreate:
if s.MaxMessageCount != 0 {
err = s.MessageAdd(t.Message)
}
case *MessageUpdate:
if s.MaxMessageCount != 0 {
err = s.MessageAdd(t.Message)
}
case *MessageDelete:
if s.MaxMessageCount != 0 {
err = s.MessageRemove(t.Message)
}
case *VoiceStateUpdate:
if s.TrackVoice {
err = s.voiceStateUpdate(t)
}
}
return
}
// UserChannelPermissions returns the permission of a user in a channel.
// userID : The ID of the user to calculate permissions for.
// channelID : The ID of the channel to calculate permission for.
func (s *State) UserChannelPermissions(userID, channelID string) (apermissions int, err error) {
channel, err := s.Channel(channelID)
if err != nil {
return
}
guild, err := s.Guild(channel.GuildID)
if err != nil {
return
}
if userID == guild.OwnerID {
apermissions = PermissionAll
return
}
member, err := s.Member(guild.ID, userID)
if err != nil {
return
}
for _, role := range guild.Roles {
for _, roleID := range member.Roles {
if role.ID == roleID {
apermissions |= role.Permissions
break
}
}
}
if apermissions&PermissionManageRoles > 0 {
apermissions |= PermissionAll
}
// Member overwrites can override role overrides, so do two passes
for _, overwrite := range channel.PermissionOverwrites {
for _, roleID := range member.Roles {
if overwrite.Type == "role" && roleID == overwrite.ID {
apermissions &= ^overwrite.Deny
apermissions |= overwrite.Allow
break
}
}
}
for _, overwrite := range channel.PermissionOverwrites {
if overwrite.Type == "member" && overwrite.ID == userID {
apermissions &= ^overwrite.Deny
apermissions |= overwrite.Allow
break
}
}
if apermissions&PermissionManageRoles > 0 {
apermissions |= PermissionAllChannel
}
return
}

521
vendor/github.com/bwmarrin/discordgo/structs.go generated vendored Normal file
View File

@ -0,0 +1,521 @@
// Discordgo - Discord bindings for Go
// Available at https://github.com/bwmarrin/discordgo
// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains all structures for the discordgo package. These
// may be moved about later into separate files but I find it easier to have
// them all located together.
package discordgo
import (
"encoding/json"
"reflect"
"sync"
"time"
"github.com/gorilla/websocket"
)
// A Session represents a connection to the Discord API.
type Session struct {
sync.RWMutex
// General configurable settings.
// Authentication token for this session
Token string
// Debug for printing JSON request/responses
Debug bool // Deprecated, will be removed.
LogLevel int
// Should the session reconnect the websocket on errors.
ShouldReconnectOnError bool
// Should the session request compressed websocket data.
Compress bool
// Sharding
ShardID int
ShardCount int
// Should state tracking be enabled.
// State tracking is the best way for getting the the users
// active guilds and the members of the guilds.
StateEnabled bool
// Exposed but should not be modified by User.
// Whether the Data Websocket is ready
DataReady bool // NOTE: Maye be deprecated soon
// Status stores the currect status of the websocket connection
// this is being tested, may stay, may go away.
status int32
// Whether the Voice Websocket is ready
VoiceReady bool // NOTE: Deprecated.
// Whether the UDP Connection is ready
UDPReady bool // NOTE: Deprecated
// Stores a mapping of guild id's to VoiceConnections
VoiceConnections map[string]*VoiceConnection
// Managed state object, updated internally with events when
// StateEnabled is true.
State *State
handlersMu sync.RWMutex
// This is a mapping of event struct to a reflected value
// for event handlers.
// We store the reflected value instead of the function
// reference as it is more performant, instead of re-reflecting
// the function each event.
handlers map[interface{}][]reflect.Value
// The websocket connection.
wsConn *websocket.Conn
// When nil, the session is not listening.
listening chan interface{}
// used to deal with rate limits
// may switch to slices later
// TODO: performance test map vs slices
rateLimit rateLimitMutex
// sequence tracks the current gateway api websocket sequence number
sequence int
// stores sessions current Discord Gateway
gateway string
// stores session ID of current Gateway connection
sessionID string
// used to make sure gateway websocket writes do not happen concurrently
wsMutex sync.Mutex
}
type rateLimitMutex struct {
sync.Mutex
url map[string]*sync.Mutex
// bucket map[string]*sync.Mutex // TODO :)
}
// A Resumed struct holds the data received in a RESUMED event
type Resumed struct {
HeartbeatInterval time.Duration `json:"heartbeat_interval"`
Trace []string `json:"_trace"`
}
// A VoiceRegion stores data for a specific voice region server.
type VoiceRegion struct {
ID string `json:"id"`
Name string `json:"name"`
Hostname string `json:"sample_hostname"`
Port int `json:"sample_port"`
}
// A VoiceICE stores data for voice ICE servers.
type VoiceICE struct {
TTL string `json:"ttl"`
Servers []*ICEServer `json:"servers"`
}
// A ICEServer stores data for a specific voice ICE server.
type ICEServer struct {
URL string `json:"url"`
Username string `json:"username"`
Credential string `json:"credential"`
}
// A Invite stores all data related to a specific Discord Guild or Channel invite.
type Invite struct {
Guild *Guild `json:"guild"`
Channel *Channel `json:"channel"`
Inviter *User `json:"inviter"`
Code string `json:"code"`
CreatedAt string `json:"created_at"` // TODO make timestamp
MaxAge int `json:"max_age"`
Uses int `json:"uses"`
MaxUses int `json:"max_uses"`
XkcdPass string `json:"xkcdpass"`
Revoked bool `json:"revoked"`
Temporary bool `json:"temporary"`
}
// A Channel holds all data related to an individual Discord channel.
type Channel struct {
ID string `json:"id"`
GuildID string `json:"guild_id"`
Name string `json:"name"`
Topic string `json:"topic"`
Type string `json:"type"`
LastMessageID string `json:"last_message_id"`
Position int `json:"position"`
Bitrate int `json:"bitrate"`
IsPrivate bool `json:"is_private"`
Recipient *User `json:"recipient"`
Messages []*Message `json:"-"`
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"`
}
// A PermissionOverwrite holds permission overwrite data for a Channel
type PermissionOverwrite struct {
ID string `json:"id"`
Type string `json:"type"`
Deny int `json:"deny"`
Allow int `json:"allow"`
}
// Emoji struct holds data related to Emoji's
type Emoji struct {
ID string `json:"id"`
Name string `json:"name"`
Roles []string `json:"roles"`
Managed bool `json:"managed"`
RequireColons bool `json:"require_colons"`
}
// VerificationLevel type defination
type VerificationLevel int
// Constants for VerificationLevel levels from 0 to 3 inclusive
const (
VerificationLevelNone VerificationLevel = iota
VerificationLevelLow
VerificationLevelMedium
VerificationLevelHigh
)
// A Guild holds all data related to a specific Discord Guild. Guilds are also
// sometimes referred to as Servers in the Discord client.
type Guild struct {
ID string `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
Region string `json:"region"`
AfkChannelID string `json:"afk_channel_id"`
EmbedChannelID string `json:"embed_channel_id"`
OwnerID string `json:"owner_id"`
JoinedAt string `json:"joined_at"` // make this a timestamp
Splash string `json:"splash"`
AfkTimeout int `json:"afk_timeout"`
VerificationLevel VerificationLevel `json:"verification_level"`
EmbedEnabled bool `json:"embed_enabled"`
Large bool `json:"large"` // ??
DefaultMessageNotifications int `json:"default_message_notifications"`
Roles []*Role `json:"roles"`
Emojis []*Emoji `json:"emojis"`
Members []*Member `json:"members"`
Presences []*Presence `json:"presences"`
Channels []*Channel `json:"channels"`
VoiceStates []*VoiceState `json:"voice_states"`
Unavailable *bool `json:"unavailable"`
}
// A GuildParams stores all the data needed to update discord guild settings
type GuildParams struct {
Name string `json:"name"`
Region string `json:"region"`
VerificationLevel *VerificationLevel `json:"verification_level"`
}
// A Role stores information about Discord guild member roles.
type Role struct {
ID string `json:"id"`
Name string `json:"name"`
Managed bool `json:"managed"`
Hoist bool `json:"hoist"`
Color int `json:"color"`
Position int `json:"position"`
Permissions int `json:"permissions"`
}
// A VoiceState stores the voice states of Guilds
type VoiceState struct {
UserID string `json:"user_id"`
SessionID string `json:"session_id"`
ChannelID string `json:"channel_id"`
GuildID string `json:"guild_id"`
Suppress bool `json:"suppress"`
SelfMute bool `json:"self_mute"`
SelfDeaf bool `json:"self_deaf"`
Mute bool `json:"mute"`
Deaf bool `json:"deaf"`
}
// A Presence stores the online, offline, or idle and game status of Guild members.
type Presence struct {
User *User `json:"user"`
Status string `json:"status"`
Game *Game `json:"game"`
}
// A Game struct holds the name of the "playing .." game for a user
type Game struct {
Name string `json:"name"`
Type int `json:"type"`
URL string `json:"url"`
}
// A Member stores user information for Guild members.
type Member struct {
GuildID string `json:"guild_id"`
JoinedAt string `json:"joined_at"`
Nick string `json:"nick"`
Deaf bool `json:"deaf"`
Mute bool `json:"mute"`
User *User `json:"user"`
Roles []string `json:"roles"`
}
// A User stores all data for an individual Discord user.
type User struct {
ID string `json:"id"`
Email string `json:"email"`
Username string `json:"username"`
Avatar string `json:"Avatar"`
Discriminator string `json:"discriminator"`
Token string `json:"token"`
Verified bool `json:"verified"`
MFAEnabled bool `json:"mfa_enabled"`
Bot bool `json:"bot"`
}
// A Settings stores data for a specific users Discord client settings.
type Settings struct {
RenderEmbeds bool `json:"render_embeds"`
InlineEmbedMedia bool `json:"inline_embed_media"`
InlineAttachmentMedia bool `json:"inline_attachment_media"`
EnableTtsCommand bool `json:"enable_tts_command"`
MessageDisplayCompact bool `json:"message_display_compact"`
ShowCurrentGame bool `json:"show_current_game"`
AllowEmailFriendRequest bool `json:"allow_email_friend_request"`
ConvertEmoticons bool `json:"convert_emoticons"`
Locale string `json:"locale"`
Theme string `json:"theme"`
GuildPositions []string `json:"guild_positions"`
RestrictedGuilds []string `json:"restricted_guilds"`
FriendSourceFlags *FriendSourceFlags `json:"friend_source_flags"`
}
// FriendSourceFlags stores ... TODO :)
type FriendSourceFlags struct {
All bool `json:"all"`
MutualGuilds bool `json:"mutual_guilds"`
MutualFriends bool `json:"mutual_friends"`
}
// An Event provides a basic initial struct for all websocket event.
type Event struct {
Operation int `json:"op"`
Sequence int `json:"s"`
Type string `json:"t"`
RawData json.RawMessage `json:"d"`
Struct interface{} `json:"-"`
}
// A Ready stores all data for the websocket READY event.
type Ready struct {
Version int `json:"v"`
SessionID string `json:"session_id"`
HeartbeatInterval time.Duration `json:"heartbeat_interval"`
User *User `json:"user"`
ReadState []*ReadState `json:"read_state"`
PrivateChannels []*Channel `json:"private_channels"`
Guilds []*Guild `json:"guilds"`
// Undocumented fields
Settings *Settings `json:"user_settings"`
UserGuildSettings []*UserGuildSettings `json:"user_guild_settings"`
Relationships []*Relationship `json:"relationships"`
Presences []*Presence `json:"presences"`
}
// A Relationship between the logged in user and Relationship.User
type Relationship struct {
User *User `json:"user"`
Type int `json:"type"` // 1 = friend, 2 = blocked, 3 = incoming friend req, 4 = sent friend req
ID string `json:"id"`
}
// A TooManyRequests struct holds information received from Discord
// when receiving a HTTP 429 response.
type TooManyRequests struct {
Bucket string `json:"bucket"`
Message string `json:"message"`
RetryAfter time.Duration `json:"retry_after"`
}
// A ReadState stores data on the read state of channels.
type ReadState struct {
MentionCount int `json:"mention_count"`
LastMessageID string `json:"last_message_id"`
ID string `json:"id"`
}
// A TypingStart stores data for the typing start websocket event.
type TypingStart struct {
UserID string `json:"user_id"`
ChannelID string `json:"channel_id"`
Timestamp int `json:"timestamp"`
}
// A PresenceUpdate stores data for the presence update websocket event.
type PresenceUpdate struct {
Presence
GuildID string `json:"guild_id"`
Roles []string `json:"roles"`
}
// A MessageAck stores data for the message ack websocket event.
type MessageAck struct {
MessageID string `json:"message_id"`
ChannelID string `json:"channel_id"`
}
// A GuildIntegrationsUpdate stores data for the guild integrations update
// websocket event.
type GuildIntegrationsUpdate struct {
GuildID string `json:"guild_id"`
}
// A GuildRole stores data for guild role websocket events.
type GuildRole struct {
Role *Role `json:"role"`
GuildID string `json:"guild_id"`
}
// A GuildRoleDelete stores data for the guild role delete websocket event.
type GuildRoleDelete struct {
RoleID string `json:"role_id"`
GuildID string `json:"guild_id"`
}
// A GuildBan stores data for a guild ban.
type GuildBan struct {
User *User `json:"user"`
GuildID string `json:"guild_id"`
}
// A GuildEmojisUpdate stores data for a guild emoji update event.
type GuildEmojisUpdate struct {
GuildID string `json:"guild_id"`
Emojis []*Emoji `json:"emojis"`
}
// A GuildIntegration stores data for a guild integration.
type GuildIntegration struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Enabled bool `json:"enabled"`
Syncing bool `json:"syncing"`
RoleID string `json:"role_id"`
ExpireBehavior int `json:"expire_behavior"`
ExpireGracePeriod int `json:"expire_grace_period"`
User *User `json:"user"`
Account *GuildIntegrationAccount `json:"account"`
SyncedAt int `json:"synced_at"`
}
// A GuildIntegrationAccount stores data for a guild integration account.
type GuildIntegrationAccount struct {
ID string `json:"id"`
Name string `json:"name"`
}
// A GuildEmbed stores data for a guild embed.
type GuildEmbed struct {
Enabled bool `json:"enabled"`
ChannelID string `json:"channel_id"`
}
// A UserGuildSettingsChannelOverride stores data for a channel override for a users guild settings.
type UserGuildSettingsChannelOverride struct {
Muted bool `json:"muted"`
MessageNotifications int `json:"message_notifications"`
ChannelID string `json:"channel_id"`
}
// A UserGuildSettings stores data for a users guild settings.
type UserGuildSettings struct {
SupressEveryone bool `json:"suppress_everyone"`
Muted bool `json:"muted"`
MobilePush bool `json:"mobile_push"`
MessageNotifications int `json:"message_notifications"`
GuildID string `json:"guild_id"`
ChannelOverrides []*UserGuildSettingsChannelOverride `json:"channel_overrides"`
}
// A UserGuildSettingsEdit stores data for editing UserGuildSettings
type UserGuildSettingsEdit struct {
SupressEveryone bool `json:"suppress_everyone"`
Muted bool `json:"muted"`
MobilePush bool `json:"mobile_push"`
MessageNotifications int `json:"message_notifications"`
ChannelOverrides map[string]*UserGuildSettingsChannelOverride `json:"channel_overrides"`
}
// Constants for the different bit offsets of text channel permissions
const (
PermissionReadMessages = 1 << (iota + 10)
PermissionSendMessages
PermissionSendTTSMessages
PermissionManageMessages
PermissionEmbedLinks
PermissionAttachFiles
PermissionReadMessageHistory
PermissionMentionEveryone
)
// Constants for the different bit offsets of voice permissions
const (
PermissionVoiceConnect = 1 << (iota + 20)
PermissionVoiceSpeak
PermissionVoiceMuteMembers
PermissionVoiceDeafenMembers
PermissionVoiceMoveMembers
PermissionVoiceUseVAD
)
// Constants for the different bit offsets of general permissions
const (
PermissionCreateInstantInvite = 1 << iota
PermissionKickMembers
PermissionBanMembers
PermissionManageRoles
PermissionManageChannels
PermissionManageServer
PermissionAllText = PermissionReadMessages |
PermissionSendMessages |
PermissionSendTTSMessages |
PermissionManageMessages |
PermissionEmbedLinks |
PermissionAttachFiles |
PermissionReadMessageHistory |
PermissionMentionEveryone
PermissionAllVoice = PermissionVoiceConnect |
PermissionVoiceSpeak |
PermissionVoiceMuteMembers |
PermissionVoiceDeafenMembers |
PermissionVoiceMoveMembers |
PermissionVoiceUseVAD
PermissionAllChannel = PermissionAllText |
PermissionAllVoice |
PermissionCreateInstantInvite |
PermissionManageRoles |
PermissionManageChannels
PermissionAll = PermissionAllChannel |
PermissionKickMembers |
PermissionBanMembers |
PermissionManageServer
)

853
vendor/github.com/bwmarrin/discordgo/voice.go generated vendored Normal file
View File

@ -0,0 +1,853 @@
// Discordgo - Discord bindings for Go
// Available at https://github.com/bwmarrin/discordgo
// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains code related to Discord voice suppport
package discordgo
import (
"encoding/binary"
"encoding/json"
"fmt"
"log"
"net"
"runtime"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
"golang.org/x/crypto/nacl/secretbox"
)
// ------------------------------------------------------------------------------------------------
// Code related to both VoiceConnection Websocket and UDP connections.
// ------------------------------------------------------------------------------------------------
// A VoiceConnection struct holds all the data and functions related to a Discord Voice Connection.
type VoiceConnection struct {
sync.RWMutex
Debug bool // If true, print extra logging -- DEPRECATED
LogLevel int
Ready bool // If true, voice is ready to send/receive audio
UserID string
GuildID string
ChannelID string
deaf bool
mute bool
speaking bool
reconnecting bool // If true, voice connection is trying to reconnect
OpusSend chan []byte // Chan for sending opus audio
OpusRecv chan *Packet // Chan for receiving opus audio
wsConn *websocket.Conn
wsMutex sync.Mutex
udpConn *net.UDPConn
session *Session
sessionID string
token string
endpoint string
// Used to send a close signal to goroutines
close chan struct{}
// Used to allow blocking until connected
connected chan bool
// Used to pass the sessionid from onVoiceStateUpdate
// sessionRecv chan string UNUSED ATM
op4 voiceOP4
op2 voiceOP2
voiceSpeakingUpdateHandlers []VoiceSpeakingUpdateHandler
}
// VoiceSpeakingUpdateHandler type provides a function defination for the
// VoiceSpeakingUpdate event
type VoiceSpeakingUpdateHandler func(vc *VoiceConnection, vs *VoiceSpeakingUpdate)
// Speaking sends a speaking notification to Discord over the voice websocket.
// This must be sent as true prior to sending audio and should be set to false
// once finished sending audio.
// b : Send true if speaking, false if not.
func (v *VoiceConnection) Speaking(b bool) (err error) {
v.log(LogDebug, "called (%t)", b)
type voiceSpeakingData struct {
Speaking bool `json:"speaking"`
Delay int `json:"delay"`
}
type voiceSpeakingOp struct {
Op int `json:"op"` // Always 5
Data voiceSpeakingData `json:"d"`
}
if v.wsConn == nil {
return fmt.Errorf("No VoiceConnection websocket.")
}
data := voiceSpeakingOp{5, voiceSpeakingData{b, 0}}
v.wsMutex.Lock()
err = v.wsConn.WriteJSON(data)
v.wsMutex.Unlock()
if err != nil {
v.speaking = false
log.Println("Speaking() write json error:", err)
return
}
v.speaking = b
return
}
// ChangeChannel sends Discord a request to change channels within a Guild
// !!! NOTE !!! This function may be removed in favour of just using ChannelVoiceJoin
func (v *VoiceConnection) ChangeChannel(channelID string, mute, deaf bool) (err error) {
v.log(LogInformational, "called")
data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, &channelID, mute, deaf}}
v.wsMutex.Lock()
err = v.session.wsConn.WriteJSON(data)
v.wsMutex.Unlock()
if err != nil {
return
}
v.ChannelID = channelID
v.deaf = deaf
v.mute = mute
v.speaking = false
return
}
// Disconnect disconnects from this voice channel and closes the websocket
// and udp connections to Discord.
// !!! NOTE !!! this function may be removed in favour of ChannelVoiceLeave
func (v *VoiceConnection) Disconnect() (err error) {
// Send a OP4 with a nil channel to disconnect
if v.sessionID != "" {
data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}}
v.wsMutex.Lock()
err = v.session.wsConn.WriteJSON(data)
v.wsMutex.Unlock()
v.sessionID = ""
}
// Close websocket and udp connections
v.Close()
v.log(LogInformational, "Deleting VoiceConnection %s", v.GuildID)
delete(v.session.VoiceConnections, v.GuildID)
return
}
// Close closes the voice ws and udp connections
func (v *VoiceConnection) Close() {
v.log(LogInformational, "called")
v.Lock()
defer v.Unlock()
v.Ready = false
v.speaking = false
if v.close != nil {
v.log(LogInformational, "closing v.close")
close(v.close)
v.close = nil
}
if v.udpConn != nil {
v.log(LogInformational, "closing udp")
err := v.udpConn.Close()
if err != nil {
log.Println("error closing udp connection: ", err)
}
v.udpConn = nil
}
if v.wsConn != nil {
v.log(LogInformational, "sending close frame")
// To cleanly close a connection, a client should send a close
// frame and wait for the server to close the connection.
err := v.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
v.log(LogError, "error closing websocket, %s", err)
}
// TODO: Wait for Discord to actually close the connection.
time.Sleep(1 * time.Second)
v.log(LogInformational, "closing websocket")
err = v.wsConn.Close()
if err != nil {
v.log(LogError, "error closing websocket, %s", err)
}
v.wsConn = nil
}
}
// AddHandler adds a Handler for VoiceSpeakingUpdate events.
func (v *VoiceConnection) AddHandler(h VoiceSpeakingUpdateHandler) {
v.Lock()
defer v.Unlock()
v.voiceSpeakingUpdateHandlers = append(v.voiceSpeakingUpdateHandlers, h)
}
// VoiceSpeakingUpdate is a struct for a VoiceSpeakingUpdate event.
type VoiceSpeakingUpdate struct {
UserID string `json:"user_id"`
SSRC int `json:"ssrc"`
Speaking bool `json:"speaking"`
}
// ------------------------------------------------------------------------------------------------
// Unexported Internal Functions Below.
// ------------------------------------------------------------------------------------------------
// A voiceOP4 stores the data for the voice operation 4 websocket event
// which provides us with the NaCl SecretBox encryption key
type voiceOP4 struct {
SecretKey [32]byte `json:"secret_key"`
Mode string `json:"mode"`
}
// A voiceOP2 stores the data for the voice operation 2 websocket event
// which is sort of like the voice READY packet
type voiceOP2 struct {
SSRC uint32 `json:"ssrc"`
Port int `json:"port"`
Modes []string `json:"modes"`
HeartbeatInterval time.Duration `json:"heartbeat_interval"`
}
// WaitUntilConnected waits for the Voice Connection to
// become ready, if it does not become ready it retuns an err
func (v *VoiceConnection) waitUntilConnected() error {
v.log(LogInformational, "called")
i := 0
for {
if v.Ready {
return nil
}
if i > 10 {
return fmt.Errorf("Timeout waiting for voice.")
}
time.Sleep(1 * time.Second)
i++
}
}
// Open opens a voice connection. This should be called
// after VoiceChannelJoin is used and the data VOICE websocket events
// are captured.
func (v *VoiceConnection) open() (err error) {
v.log(LogInformational, "called")
v.Lock()
defer v.Unlock()
// Don't open a websocket if one is already open
if v.wsConn != nil {
v.log(LogWarning, "refusing to overwrite non-nil websocket")
return
}
// TODO temp? loop to wait for the SessionID
i := 0
for {
if v.sessionID != "" {
break
}
if i > 20 { // only loop for up to 1 second total
return fmt.Errorf("Did not receive voice Session ID in time.")
}
time.Sleep(50 * time.Millisecond)
i++
}
// Connect to VoiceConnection Websocket
vg := fmt.Sprintf("wss://%s", strings.TrimSuffix(v.endpoint, ":80"))
v.log(LogInformational, "connecting to voice endpoint %s", vg)
v.wsConn, _, err = websocket.DefaultDialer.Dial(vg, nil)
if err != nil {
v.log(LogWarning, "error connecting to voice endpoint %s, %s", vg, err)
v.log(LogDebug, "voice struct: %#v\n", v)
return
}
type voiceHandshakeData struct {
ServerID string `json:"server_id"`
UserID string `json:"user_id"`
SessionID string `json:"session_id"`
Token string `json:"token"`
}
type voiceHandshakeOp struct {
Op int `json:"op"` // Always 0
Data voiceHandshakeData `json:"d"`
}
data := voiceHandshakeOp{0, voiceHandshakeData{v.GuildID, v.UserID, v.sessionID, v.token}}
err = v.wsConn.WriteJSON(data)
if err != nil {
v.log(LogWarning, "error sending init packet, %s", err)
return
}
v.close = make(chan struct{})
go v.wsListen(v.wsConn, v.close)
// add loop/check for Ready bool here?
// then return false if not ready?
// but then wsListen will also err.
return
}
// wsListen listens on the voice websocket for messages and passes them
// to the voice event handler. This is automatically called by the Open func
func (v *VoiceConnection) wsListen(wsConn *websocket.Conn, close <-chan struct{}) {
v.log(LogInformational, "called")
for {
_, message, err := v.wsConn.ReadMessage()
if err != nil {
// Detect if we have been closed manually. If a Close() has already
// happened, the websocket we are listening on will be different to the
// current session.
v.RLock()
sameConnection := v.wsConn == wsConn
v.RUnlock()
if sameConnection {
v.log(LogError, "voice endpoint %s websocket closed unexpectantly, %s", v.endpoint, err)
// Start reconnect goroutine then exit.
go v.reconnect()
}
return
}
// Pass received message to voice event handler
select {
case <-close:
return
default:
go v.onEvent(message)
}
}
}
// wsEvent handles any voice websocket events. This is only called by the
// wsListen() function.
func (v *VoiceConnection) onEvent(message []byte) {
v.log(LogDebug, "received: %s", string(message))
var e Event
if err := json.Unmarshal(message, &e); err != nil {
v.log(LogError, "unmarshall error, %s", err)
return
}
switch e.Operation {
case 2: // READY
if err := json.Unmarshal(e.RawData, &v.op2); err != nil {
v.log(LogError, "OP2 unmarshall error, %s, %s", err, string(e.RawData))
return
}
// Start the voice websocket heartbeat to keep the connection alive
go v.wsHeartbeat(v.wsConn, v.close, v.op2.HeartbeatInterval)
// TODO monitor a chan/bool to verify this was successful
// Start the UDP connection
err := v.udpOpen()
if err != nil {
v.log(LogError, "error opening udp connection, %s", err)
return
}
// Start the opusSender.
// TODO: Should we allow 48000/960 values to be user defined?
if v.OpusSend == nil {
v.OpusSend = make(chan []byte, 2)
}
go v.opusSender(v.udpConn, v.close, v.OpusSend, 48000, 960)
// Start the opusReceiver
if !v.deaf {
if v.OpusRecv == nil {
v.OpusRecv = make(chan *Packet, 2)
}
go v.opusReceiver(v.udpConn, v.close, v.OpusRecv)
}
// Send the ready event
v.connected <- true
return
case 3: // HEARTBEAT response
// add code to use this to track latency?
return
case 4: // udp encryption secret key
v.op4 = voiceOP4{}
if err := json.Unmarshal(e.RawData, &v.op4); err != nil {
v.log(LogError, "OP4 unmarshall error, %s, %s", err, string(e.RawData))
return
}
return
case 5:
if len(v.voiceSpeakingUpdateHandlers) == 0 {
return
}
voiceSpeakingUpdate := &VoiceSpeakingUpdate{}
if err := json.Unmarshal(e.RawData, voiceSpeakingUpdate); err != nil {
v.log(LogError, "OP5 unmarshall error, %s, %s", err, string(e.RawData))
return
}
for _, h := range v.voiceSpeakingUpdateHandlers {
h(v, voiceSpeakingUpdate)
}
default:
v.log(LogError, "unknown voice operation, %d, %s", e.Operation, string(e.RawData))
}
return
}
type voiceHeartbeatOp struct {
Op int `json:"op"` // Always 3
Data int `json:"d"`
}
// NOTE :: When a guild voice server changes how do we shut this down
// properly, so a new connection can be setup without fuss?
//
// wsHeartbeat sends regular heartbeats to voice Discord so it knows the client
// is still connected. If you do not send these heartbeats Discord will
// disconnect the websocket connection after a few seconds.
func (v *VoiceConnection) wsHeartbeat(wsConn *websocket.Conn, close <-chan struct{}, i time.Duration) {
if close == nil || wsConn == nil {
return
}
var err error
ticker := time.NewTicker(i * time.Millisecond)
for {
v.log(LogDebug, "sending heartbeat packet")
v.wsMutex.Lock()
err = wsConn.WriteJSON(voiceHeartbeatOp{3, int(time.Now().Unix())})
v.wsMutex.Unlock()
if err != nil {
v.log(LogError, "error sending heartbeat to voice endpoint %s, %s", v.endpoint, err)
return
}
select {
case <-ticker.C:
// continue loop and send heartbeat
case <-close:
return
}
}
}
// ------------------------------------------------------------------------------------------------
// Code related to the VoiceConnection UDP connection
// ------------------------------------------------------------------------------------------------
type voiceUDPData struct {
Address string `json:"address"` // Public IP of machine running this code
Port uint16 `json:"port"` // UDP Port of machine running this code
Mode string `json:"mode"` // always "xsalsa20_poly1305"
}
type voiceUDPD struct {
Protocol string `json:"protocol"` // Always "udp" ?
Data voiceUDPData `json:"data"`
}
type voiceUDPOp struct {
Op int `json:"op"` // Always 1
Data voiceUDPD `json:"d"`
}
// udpOpen opens a UDP connection to the voice server and completes the
// initial required handshake. This connection is left open in the session
// and can be used to send or receive audio. This should only be called
// from voice.wsEvent OP2
func (v *VoiceConnection) udpOpen() (err error) {
v.Lock()
defer v.Unlock()
if v.wsConn == nil {
return fmt.Errorf("nil voice websocket")
}
if v.udpConn != nil {
return fmt.Errorf("udp connection already open")
}
if v.close == nil {
return fmt.Errorf("nil close channel")
}
if v.endpoint == "" {
return fmt.Errorf("empty endpoint")
}
host := fmt.Sprintf("%s:%d", strings.TrimSuffix(v.endpoint, ":80"), v.op2.Port)
addr, err := net.ResolveUDPAddr("udp", host)
if err != nil {
v.log(LogWarning, "error resolving udp host %s, %s", host, err)
return
}
v.log(LogInformational, "connecting to udp addr %s", addr.String())
v.udpConn, err = net.DialUDP("udp", nil, addr)
if err != nil {
v.log(LogWarning, "error connecting to udp addr %s, %s", addr.String(), err)
return
}
// Create a 70 byte array and put the SSRC code from the Op 2 VoiceConnection event
// into it. Then send that over the UDP connection to Discord
sb := make([]byte, 70)
binary.BigEndian.PutUint32(sb, v.op2.SSRC)
_, err = v.udpConn.Write(sb)
if err != nil {
v.log(LogWarning, "udp write error to %s, %s", addr.String(), err)
return
}
// Create a 70 byte array and listen for the initial handshake response
// from Discord. Once we get it parse the IP and PORT information out
// of the response. This should be our public IP and PORT as Discord
// saw us.
rb := make([]byte, 70)
rlen, _, err := v.udpConn.ReadFromUDP(rb)
if err != nil {
v.log(LogWarning, "udp read error, %s, %s", addr.String(), err)
return
}
if rlen < 70 {
v.log(LogWarning, "received udp packet too small")
return fmt.Errorf("received udp packet too small")
}
// Loop over position 4 though 20 to grab the IP address
// Should never be beyond position 20.
var ip string
for i := 4; i < 20; i++ {
if rb[i] == 0 {
break
}
ip += string(rb[i])
}
// Grab port from position 68 and 69
port := binary.LittleEndian.Uint16(rb[68:70])
// Take the data from above and send it back to Discord to finalize
// the UDP connection handshake.
data := voiceUDPOp{1, voiceUDPD{"udp", voiceUDPData{ip, port, "xsalsa20_poly1305"}}}
v.wsMutex.Lock()
err = v.wsConn.WriteJSON(data)
v.wsMutex.Unlock()
if err != nil {
v.log(LogWarning, "udp write error, %#v, %s", data, err)
return
}
// start udpKeepAlive
go v.udpKeepAlive(v.udpConn, v.close, 5*time.Second)
// TODO: find a way to check that it fired off okay
return
}
// udpKeepAlive sends a udp packet to keep the udp connection open
// This is still a bit of a "proof of concept"
func (v *VoiceConnection) udpKeepAlive(udpConn *net.UDPConn, close <-chan struct{}, i time.Duration) {
if udpConn == nil || close == nil {
return
}
var err error
var sequence uint64
packet := make([]byte, 8)
ticker := time.NewTicker(i)
for {
binary.LittleEndian.PutUint64(packet, sequence)
sequence++
_, err = udpConn.Write(packet)
if err != nil {
v.log(LogError, "write error, %s", err)
return
}
select {
case <-ticker.C:
// continue loop and send keepalive
case <-close:
return
}
}
}
// opusSender will listen on the given channel and send any
// pre-encoded opus audio to Discord. Supposedly.
func (v *VoiceConnection) opusSender(udpConn *net.UDPConn, close <-chan struct{}, opus <-chan []byte, rate, size int) {
if udpConn == nil || close == nil {
return
}
runtime.LockOSThread()
// VoiceConnection is now ready to receive audio packets
// TODO: this needs reviewed as I think there must be a better way.
v.Ready = true
defer func() { v.Ready = false }()
var sequence uint16
var timestamp uint32
var recvbuf []byte
var ok bool
udpHeader := make([]byte, 12)
var nonce [24]byte
// build the parts that don't change in the udpHeader
udpHeader[0] = 0x80
udpHeader[1] = 0x78
binary.BigEndian.PutUint32(udpHeader[8:], v.op2.SSRC)
// start a send loop that loops until buf chan is closed
ticker := time.NewTicker(time.Millisecond * time.Duration(size/(rate/1000)))
for {
// Get data from chan. If chan is closed, return.
select {
case <-close:
return
case recvbuf, ok = <-opus:
if !ok {
return
}
// else, continue loop
}
if !v.speaking {
err := v.Speaking(true)
if err != nil {
v.log(LogError, "error sending speaking packet, %s", err)
}
}
// Add sequence and timestamp to udpPacket
binary.BigEndian.PutUint16(udpHeader[2:], sequence)
binary.BigEndian.PutUint32(udpHeader[4:], timestamp)
// encrypt the opus data
copy(nonce[:], udpHeader)
sendbuf := secretbox.Seal(udpHeader, recvbuf, &nonce, &v.op4.SecretKey)
// block here until we're exactly at the right time :)
// Then send rtp audio packet to Discord over UDP
select {
case <-close:
return
case <-ticker.C:
// continue
}
_, err := udpConn.Write(sendbuf)
if err != nil {
v.log(LogError, "udp write error, %s", err)
v.log(LogDebug, "voice struct: %#v\n", v)
return
}
if (sequence) == 0xFFFF {
sequence = 0
} else {
sequence++
}
if (timestamp + uint32(size)) >= 0xFFFFFFFF {
timestamp = 0
} else {
timestamp += uint32(size)
}
}
}
// A Packet contains the headers and content of a received voice packet.
type Packet struct {
SSRC uint32
Sequence uint16
Timestamp uint32
Type []byte
Opus []byte
PCM []int16
}
// opusReceiver listens on the UDP socket for incoming packets
// and sends them across the given channel
// NOTE :: This function may change names later.
func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct{}, c chan *Packet) {
if udpConn == nil || close == nil {
return
}
p := Packet{}
recvbuf := make([]byte, 1024)
var nonce [24]byte
for {
rlen, err := udpConn.Read(recvbuf)
if err != nil {
// Detect if we have been closed manually. If a Close() has already
// happened, the udp connection we are listening on will be different
// to the current session.
v.RLock()
sameConnection := v.udpConn == udpConn
v.RUnlock()
if sameConnection {
v.log(LogError, "udp read error, %s, %s", v.endpoint, err)
v.log(LogDebug, "voice struct: %#v\n", v)
go v.reconnect()
}
return
}
select {
case <-close:
return
default:
// continue loop
}
// For now, skip anything except audio.
if rlen < 12 || recvbuf[0] != 0x80 {
continue
}
// build a audio packet struct
p.Type = recvbuf[0:2]
p.Sequence = binary.BigEndian.Uint16(recvbuf[2:4])
p.Timestamp = binary.BigEndian.Uint32(recvbuf[4:8])
p.SSRC = binary.BigEndian.Uint32(recvbuf[8:12])
// decrypt opus data
copy(nonce[:], recvbuf[0:12])
p.Opus, _ = secretbox.Open(nil, recvbuf[12:rlen], &nonce, &v.op4.SecretKey)
if c != nil {
c <- &p
}
}
}
// Reconnect will close down a voice connection then immediately try to
// reconnect to that session.
// NOTE : This func is messy and a WIP while I find what works.
// It will be cleaned up once a proven stable option is flushed out.
// aka: this is ugly shit code, please don't judge too harshly.
func (v *VoiceConnection) reconnect() {
v.log(LogInformational, "called")
v.Lock()
if v.reconnecting {
v.log(LogInformational, "already reconnecting to channel %s, exiting", v.ChannelID)
v.Unlock()
return
}
v.reconnecting = true
v.Unlock()
defer func() { v.reconnecting = false }()
// Close any currently open connections
v.Close()
wait := time.Duration(1)
for {
<-time.After(wait * time.Second)
wait *= 2
if wait > 600 {
wait = 600
}
if v.session.DataReady == false || v.session.wsConn == nil {
v.log(LogInformational, "cannot reconenct to channel %s with unready session", v.ChannelID)
continue
}
v.log(LogInformational, "trying to reconnect to channel %s", v.ChannelID)
_, err := v.session.ChannelVoiceJoin(v.GuildID, v.ChannelID, v.mute, v.deaf)
if err == nil {
v.log(LogInformational, "successfully reconnected to channel %s", v.ChannelID)
return
}
// if the reconnect above didn't work lets just send a disconnect
// packet to reset things.
// Send a OP4 with a nil channel to disconnect
data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}}
v.session.wsMutex.Lock()
err = v.session.wsConn.WriteJSON(data)
v.session.wsMutex.Unlock()
if err != nil {
v.log(LogError, "error sending disconnect packet, %s", err)
}
v.log(LogInformational, "error reconnecting to channel %s, %s", v.ChannelID, err)
}
}

679
vendor/github.com/bwmarrin/discordgo/wsapi.go generated vendored Normal file
View File

@ -0,0 +1,679 @@
// Discordgo - Discord bindings for Go
// Available at https://github.com/bwmarrin/discordgo
// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains low level functions for interacting with the Discord
// data websocket interface.
package discordgo
import (
"bytes"
"compress/zlib"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"reflect"
"runtime"
"time"
"github.com/gorilla/websocket"
)
type resumePacket struct {
Op int `json:"op"`
Data struct {
Token string `json:"token"`
SessionID string `json:"session_id"`
Sequence int `json:"seq"`
} `json:"d"`
}
// Open opens a websocket connection to Discord.
func (s *Session) Open() (err error) {
s.log(LogInformational, "called")
s.Lock()
defer func() {
if err != nil {
s.Unlock()
}
}()
if s.wsConn != nil {
err = errors.New("Web socket already opened.")
return
}
if s.VoiceConnections == nil {
s.log(LogInformational, "creating new VoiceConnections map")
s.VoiceConnections = make(map[string]*VoiceConnection)
}
// Get the gateway to use for the Websocket connection
if s.gateway == "" {
s.gateway, err = s.Gateway()
if err != nil {
return
}
// Add the version and encoding to the URL
s.gateway = fmt.Sprintf("%s?v=4&encoding=json", s.gateway)
}
header := http.Header{}
header.Add("accept-encoding", "zlib")
s.log(LogInformational, "connecting to gateway %s", s.gateway)
s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header)
if err != nil {
s.log(LogWarning, "error connecting to gateway %s, %s", s.gateway, err)
s.gateway = "" // clear cached gateway
// TODO: should we add a retry block here?
return
}
if s.sessionID != "" && s.sequence > 0 {
p := resumePacket{}
p.Op = 6
p.Data.Token = s.Token
p.Data.SessionID = s.sessionID
p.Data.Sequence = s.sequence
s.log(LogInformational, "sending resume packet to gateway")
err = s.wsConn.WriteJSON(p)
if err != nil {
s.log(LogWarning, "error sending gateway resume packet, %s, %s", s.gateway, err)
return
}
} else {
err = s.identify()
if err != nil {
s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err)
return
}
}
// Create listening outside of listen, as it needs to happen inside the mutex
// lock.
s.listening = make(chan interface{})
go s.listen(s.wsConn, s.listening)
s.Unlock()
s.initialize()
s.log(LogInformational, "emit connect event")
s.handle(&Connect{})
s.log(LogInformational, "exiting")
return
}
// listen polls the websocket connection for events, it will stop when the
// listening channel is closed, or an error occurs.
func (s *Session) listen(wsConn *websocket.Conn, listening <-chan interface{}) {
s.log(LogInformational, "called")
for {
messageType, message, err := wsConn.ReadMessage()
if err != nil {
// Detect if we have been closed manually. If a Close() has already
// happened, the websocket we are listening on will be different to
// the current session.
s.RLock()
sameConnection := s.wsConn == wsConn
s.RUnlock()
if sameConnection {
s.log(LogWarning, "error reading from gateway %s websocket, %s", s.gateway, err)
// There has been an error reading, close the websocket so that
// OnDisconnect event is emitted.
err := s.Close()
if err != nil {
s.log(LogWarning, "error closing session connection, %s", err)
}
s.log(LogInformational, "calling reconnect() now")
s.reconnect()
}
return
}
select {
case <-listening:
return
default:
s.onEvent(messageType, message)
}
}
}
type heartbeatOp struct {
Op int `json:"op"`
Data int `json:"d"`
}
// heartbeat sends regular heartbeats to Discord so it knows the client
// is still connected. If you do not send these heartbeats Discord will
// disconnect the websocket connection after a few seconds.
func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, i time.Duration) {
s.log(LogInformational, "called")
if listening == nil || wsConn == nil {
return
}
var err error
ticker := time.NewTicker(i * time.Millisecond)
for {
s.log(LogInformational, "sending gateway websocket heartbeat seq %d", s.sequence)
s.wsMutex.Lock()
err = wsConn.WriteJSON(heartbeatOp{1, s.sequence})
s.wsMutex.Unlock()
if err != nil {
s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err)
s.Lock()
s.DataReady = false
s.Unlock()
return
}
s.Lock()
s.DataReady = true
s.Unlock()
select {
case <-ticker.C:
// continue loop and send heartbeat
case <-listening:
return
}
}
}
type updateStatusData struct {
IdleSince *int `json:"idle_since"`
Game *Game `json:"game"`
}
type updateStatusOp struct {
Op int `json:"op"`
Data updateStatusData `json:"d"`
}
// UpdateStreamingStatus is used to update the user's streaming status.
// If idle>0 then set status to idle.
// If game!="" then set game.
// If game!="" and url!="" then set the status type to streaming with the URL set.
// if otherwise, set status to active, and no game.
func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err error) {
s.log(LogInformational, "called")
s.RLock()
defer s.RUnlock()
if s.wsConn == nil {
return errors.New("no websocket connection exists")
}
var usd updateStatusData
if idle > 0 {
usd.IdleSince = &idle
}
if game != "" {
gameType := 0
if url != "" {
gameType = 1
}
usd.Game = &Game{
Name: game,
Type: gameType,
URL: url,
}
}
s.wsMutex.Lock()
err = s.wsConn.WriteJSON(updateStatusOp{3, usd})
s.wsMutex.Unlock()
return
}
// UpdateStatus is used to update the user's status.
// If idle>0 then set status to idle.
// If game!="" then set game.
// if otherwise, set status to active, and no game.
func (s *Session) UpdateStatus(idle int, game string) (err error) {
return s.UpdateStreamingStatus(idle, game, "")
}
// onEvent is the "event handler" for all messages received on the
// Discord Gateway API websocket connection.
//
// If you use the AddHandler() function to register a handler for a
// specific event this function will pass the event along to that handler.
//
// If you use the AddHandler() function to register a handler for the
// "OnEvent" event then all events will be passed to that handler.
//
// TODO: You may also register a custom event handler entirely using...
func (s *Session) onEvent(messageType int, message []byte) {
var err error
var reader io.Reader
reader = bytes.NewBuffer(message)
// If this is a compressed message, uncompress it.
if messageType == websocket.BinaryMessage {
z, err2 := zlib.NewReader(reader)
if err2 != nil {
s.log(LogError, "error uncompressing websocket message, %s", err)
return
}
defer func() {
err3 := z.Close()
if err3 != nil {
s.log(LogWarning, "error closing zlib, %s", err)
}
}()
reader = z
}
// Decode the event into an Event struct.
var e *Event
decoder := json.NewDecoder(reader)
if err = decoder.Decode(&e); err != nil {
s.log(LogError, "error decoding websocket message, %s", err)
return
}
s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s\n\n", e.Operation, e.Sequence, e.Type, string(e.RawData))
// Ping request.
// Must respond with a heartbeat packet within 5 seconds
if e.Operation == 1 {
s.log(LogInformational, "sending heartbeat in response to Op1")
s.wsMutex.Lock()
err = s.wsConn.WriteJSON(heartbeatOp{1, s.sequence})
s.wsMutex.Unlock()
if err != nil {
s.log(LogError, "error sending heartbeat in response to Op1")
return
}
return
}
// Reconnect
// Must immediately disconnect from gateway and reconnect to new gateway.
if e.Operation == 7 {
// TODO
}
// Invalid Session
// Must respond with a Identify packet.
if e.Operation == 9 {
s.log(LogInformational, "sending identify packet to gateway in response to Op9")
err = s.identify()
if err != nil {
s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err)
return
}
return
}
// Do not try to Dispatch a non-Dispatch Message
if e.Operation != 0 {
// But we probably should be doing something with them.
// TEMP
s.log(LogWarning, "unknown Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message))
return
}
// Store the message sequence
s.sequence = e.Sequence
// Map event to registered event handlers and pass it along
// to any registered functions
i := eventToInterface[e.Type]
if i != nil {
// Create a new instance of the event type.
i = reflect.New(reflect.TypeOf(i)).Interface()
// Attempt to unmarshal our event.
if err = json.Unmarshal(e.RawData, i); err != nil {
s.log(LogError, "error unmarshalling %s event, %s", e.Type, err)
}
// Send event to any registered event handlers for it's type.
// Because the above doesn't cancel this, in case of an error
// the struct could be partially populated or at default values.
// However, most errors are due to a single field and I feel
// it's better to pass along what we received than nothing at all.
// TODO: Think about that decision :)
// Either way, READY events must fire, even with errors.
go s.handle(i)
} else {
s.log(LogWarning, "unknown event: Op: %d, Seq: %d, Type: %s, Data: %s", e.Operation, e.Sequence, e.Type, string(e.RawData))
}
// Emit event to the OnEvent handler
e.Struct = i
go s.handle(e)
}
// ------------------------------------------------------------------------------------------------
// Code related to voice connections that initiate over the data websocket
// ------------------------------------------------------------------------------------------------
// A VoiceServerUpdate stores the data received during the Voice Server Update
// data websocket event. This data is used during the initial Voice Channel
// join handshaking.
type VoiceServerUpdate struct {
Token string `json:"token"`
GuildID string `json:"guild_id"`
Endpoint string `json:"endpoint"`
}
type voiceChannelJoinData struct {
GuildID *string `json:"guild_id"`
ChannelID *string `json:"channel_id"`
SelfMute bool `json:"self_mute"`
SelfDeaf bool `json:"self_deaf"`
}
type voiceChannelJoinOp struct {
Op int `json:"op"`
Data voiceChannelJoinData `json:"d"`
}
// ChannelVoiceJoin joins the session user to a voice channel.
//
// gID : Guild ID of the channel to join.
// cID : Channel ID of the channel to join.
// mute : If true, you will be set to muted upon joining.
// deaf : If true, you will be set to deafened upon joining.
func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *VoiceConnection, err error) {
s.log(LogInformational, "called")
voice, _ = s.VoiceConnections[gID]
if voice == nil {
voice = &VoiceConnection{}
s.VoiceConnections[gID] = voice
}
voice.GuildID = gID
voice.ChannelID = cID
voice.deaf = deaf
voice.mute = mute
voice.session = s
// Send the request to Discord that we want to join the voice channel
data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}}
s.wsMutex.Lock()
err = s.wsConn.WriteJSON(data)
s.wsMutex.Unlock()
if err != nil {
return
}
// doesn't exactly work perfect yet.. TODO
err = voice.waitUntilConnected()
if err != nil {
s.log(LogWarning, "error waiting for voice to connect, %s", err)
voice.Close()
return
}
return
}
// onVoiceStateUpdate handles Voice State Update events on the data websocket.
func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) {
// If we don't have a connection for the channel, don't bother
if st.ChannelID == "" {
return
}
// Check if we have a voice connection to update
voice, exists := s.VoiceConnections[st.GuildID]
if !exists {
return
}
// Need to have this happen at login and store it in the Session
// TODO : This should be done upon connecting to Discord, or
// be moved to a small helper function
self, err := s.User("@me") // TODO: move to Login/New
if err != nil {
log.Println(err)
return
}
// We only care about events that are about us
if st.UserID != self.ID {
return
}
// Store the SessionID for later use.
voice.UserID = self.ID // TODO: Review
voice.sessionID = st.SessionID
}
// onVoiceServerUpdate handles the Voice Server Update data websocket event.
//
// This is also fired if the Guild's voice region changes while connected
// to a voice channel. In that case, need to re-establish connection to
// the new region endpoint.
func (s *Session) onVoiceServerUpdate(se *Session, st *VoiceServerUpdate) {
s.log(LogInformational, "called")
voice, exists := s.VoiceConnections[st.GuildID]
// If no VoiceConnection exists, just skip this
if !exists {
return
}
// If currently connected to voice ws/udp, then disconnect.
// Has no effect if not connected.
voice.Close()
// Store values for later use
voice.token = st.Token
voice.endpoint = st.Endpoint
voice.GuildID = st.GuildID
// Open a conenction to the voice server
err := voice.open()
if err != nil {
s.log(LogError, "onVoiceServerUpdate voice.open, %s", err)
}
}
type identifyProperties struct {
OS string `json:"$os"`
Browser string `json:"$browser"`
Device string `json:"$device"`
Referer string `json:"$referer"`
ReferringDomain string `json:"$referring_domain"`
}
type identifyData struct {
Token string `json:"token"`
Properties identifyProperties `json:"properties"`
LargeThreshold int `json:"large_threshold"`
Compress bool `json:"compress"`
Shard *[2]int `json:"shard,omitempty"`
}
type identifyOp struct {
Op int `json:"op"`
Data identifyData `json:"d"`
}
// identify sends the identify packet to the gateway
func (s *Session) identify() error {
properties := identifyProperties{runtime.GOOS,
"Discordgo v" + VERSION,
"",
"",
"",
}
data := identifyData{s.Token,
properties,
250,
s.Compress,
nil,
}
if s.ShardCount > 1 {
if s.ShardID >= s.ShardCount {
return errors.New("ShardID must be less than ShardCount")
}
data.Shard = &[2]int{s.ShardID, s.ShardCount}
}
op := identifyOp{2, data}
s.wsMutex.Lock()
err := s.wsConn.WriteJSON(op)
s.wsMutex.Unlock()
if err != nil {
return err
}
return nil
}
func (s *Session) reconnect() {
s.log(LogInformational, "called")
var err error
if s.ShouldReconnectOnError {
wait := time.Duration(1)
for {
s.log(LogInformational, "trying to reconnect to gateway")
err = s.Open()
if err == nil {
s.log(LogInformational, "successfully reconnected to gateway")
// I'm not sure if this is actually needed.
// if the gw reconnect works properly, voice should stay alive
// However, there seems to be cases where something "weird"
// happens. So we're doing this for now just to improve
// stability in those edge cases.
for _, v := range s.VoiceConnections {
s.log(LogInformational, "reconnecting voice connection to guild %s", v.GuildID)
go v.reconnect()
// This is here just to prevent violently spamming the
// voice reconnects
time.Sleep(1 * time.Second)
}
return
}
s.log(LogError, "error reconnecting to gateway, %s", err)
<-time.After(wait * time.Second)
wait *= 2
if wait > 600 {
wait = 600
}
}
}
}
// Close closes a websocket and stops all listening/heartbeat goroutines.
// TODO: Add support for Voice WS/UDP connections
func (s *Session) Close() (err error) {
s.log(LogInformational, "called")
s.Lock()
s.DataReady = false
if s.listening != nil {
s.log(LogInformational, "closing listening channel")
close(s.listening)
s.listening = nil
}
// TODO: Close all active Voice Connections too
// this should force stop any reconnecting voice channels too
if s.wsConn != nil {
s.log(LogInformational, "sending close frame")
// To cleanly close a connection, a client should send a close
// frame and wait for the server to close the connection.
err := s.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
s.log(LogError, "error closing websocket, %s", err)
}
// TODO: Wait for Discord to actually close the connection.
time.Sleep(1 * time.Second)
s.log(LogInformational, "closing gateway websocket")
err = s.wsConn.Close()
if err != nil {
s.log(LogError, "error closing websocket, %s", err)
}
s.wsConn = nil
}
s.Unlock()
s.log(LogInformational, "emit disconnect event")
s.handle(&Disconnect{})
return
}

View File

@ -13,7 +13,6 @@ type ClusterInterface interface {
GetClusterInfos() []*model.ClusterInfo
RemoveAllSessionsForUserId(userId string)
InvalidateCacheForUser(userId string)
InvalidateCacheForChannel(channelId string)
Publish(event *model.WebSocketEvent)
UpdateStatus(status *model.Status)
GetLogs() ([]string, *model.AppError)

View File

@ -8,7 +8,7 @@ import (
)
type MfaInterface interface {
GenerateQrCode(user *model.User) ([]byte, *model.AppError)
GenerateSecret(user *model.User) (string, []byte, *model.AppError)
Activate(user *model.User, token string) *model.AppError
Deactivate(userId string) *model.AppError
ValidateToken(secret, token string) (bool, *model.AppError)

View File

@ -0,0 +1,372 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
type Permission struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
type Role struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Permissions []string `json:"permissions"`
}
var PERMISSION_INVITE_USER *Permission
var PERMISSION_ADD_USER_TO_TEAM *Permission
var PERMISSION_USE_SLASH_COMMANDS *Permission
var PERMISSION_MANAGE_SLASH_COMMANDS *Permission
var PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS *Permission
var PERMISSION_CREATE_PUBLIC_CHANNEL *Permission
var PERMISSION_CREATE_PRIVATE_CHANNEL *Permission
var PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS *Permission
var PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS *Permission
var PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE *Permission
var PERMISSION_MANAGE_ROLES *Permission
var PERMISSION_CREATE_DIRECT_CHANNEL *Permission
var PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES *Permission
var PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES *Permission
var PERMISSION_LIST_TEAM_CHANNELS *Permission
var PERMISSION_JOIN_PUBLIC_CHANNELS *Permission
var PERMISSION_DELETE_PUBLIC_CHANNEL *Permission
var PERMISSION_DELETE_PRIVATE_CHANNEL *Permission
var PERMISSION_EDIT_OTHER_USERS *Permission
var PERMISSION_READ_CHANNEL *Permission
var PERMISSION_PERMANENT_DELETE_USER *Permission
var PERMISSION_UPLOAD_FILE *Permission
var PERMISSION_GET_PUBLIC_LINK *Permission
var PERMISSION_MANAGE_WEBHOOKS *Permission
var PERMISSION_MANAGE_OTHERS_WEBHOOKS *Permission
var PERMISSION_MANAGE_OAUTH *Permission
var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission
var PERMISSION_CREATE_POST *Permission
var PERMISSION_EDIT_POST *Permission
var PERMISSION_EDIT_OTHERS_POSTS *Permission
var PERMISSION_REMOVE_USER_FROM_TEAM *Permission
var PERMISSION_MANAGE_TEAM *Permission
var PERMISSION_IMPORT_TEAM *Permission
// General permission that encompases all system admin functions
// in the future this could be broken up to allow access to some
// admin functions but not others
var PERMISSION_MANAGE_SYSTEM *Permission
var ROLE_SYSTEM_USER *Role
var ROLE_SYSTEM_ADMIN *Role
var ROLE_TEAM_USER *Role
var ROLE_TEAM_ADMIN *Role
var ROLE_CHANNEL_USER *Role
var ROLE_CHANNEL_ADMIN *Role
var ROLE_CHANNEL_GUEST *Role
var BuiltInRoles map[string]*Role
func InitalizePermissions() {
PERMISSION_INVITE_USER = &Permission{
"invite_user",
"authentication.permissions.team_invite_user.name",
"authentication.permissions.team_invite_user.description",
}
PERMISSION_ADD_USER_TO_TEAM = &Permission{
"add_user_to_team",
"authentication.permissions.add_user_to_team.name",
"authentication.permissions.add_user_to_team.description",
}
PERMISSION_USE_SLASH_COMMANDS = &Permission{
"use_slash_commands",
"authentication.permissions.team_use_slash_commands.name",
"authentication.permissions.team_use_slash_commands.description",
}
PERMISSION_MANAGE_SLASH_COMMANDS = &Permission{
"manage_slash_commands",
"authentication.permissions.manage_slash_commands.name",
"authentication.permissions.manage_slash_commands.description",
}
PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS = &Permission{
"manage_others_slash_commands",
"authentication.permissions.manage_others_slash_commands.name",
"authentication.permissions.manage_others_slash_commands.description",
}
PERMISSION_CREATE_PUBLIC_CHANNEL = &Permission{
"create_public_channel",
"authentication.permissions.create_public_channel.name",
"authentication.permissions.create_public_channel.description",
}
PERMISSION_CREATE_PRIVATE_CHANNEL = &Permission{
"create_private_channel",
"authentication.permissions.create_private_channel.name",
"authentication.permissions.create_private_channel.description",
}
PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS = &Permission{
"manage_public_channel_members",
"authentication.permissions.manage_public_channel_members.name",
"authentication.permissions.manage_public_channel_members.description",
}
PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS = &Permission{
"manage_private_channel_members",
"authentication.permissions.manage_private_channel_members.name",
"authentication.permissions.manage_private_channel_members.description",
}
PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE = &Permission{
"assign_system_admin_role",
"authentication.permissions.assign_system_admin_role.name",
"authentication.permissions.assign_system_admin_role.description",
}
PERMISSION_MANAGE_ROLES = &Permission{
"manage_roles",
"authentication.permissions.manage_roles.name",
"authentication.permissions.manage_roles.description",
}
PERMISSION_MANAGE_SYSTEM = &Permission{
"manage_system",
"authentication.permissions.manage_system.name",
"authentication.permissions.manage_system.description",
}
PERMISSION_CREATE_DIRECT_CHANNEL = &Permission{
"create_direct_channel",
"authentication.permissions.create_direct_channel.name",
"authentication.permissions.create_direct_channel.description",
}
PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES = &Permission{
"manage__publicchannel_properties",
"authentication.permissions.manage_public_channel_properties.name",
"authentication.permissions.manage_public_channel_properties.description",
}
PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES = &Permission{
"manage_private_channel_properties",
"authentication.permissions.manage_private_channel_properties.name",
"authentication.permissions.manage_private_channel_properties.description",
}
PERMISSION_LIST_TEAM_CHANNELS = &Permission{
"list_team_channels",
"authentication.permissions.list_team_channels.name",
"authentication.permissions.list_team_channels.description",
}
PERMISSION_JOIN_PUBLIC_CHANNELS = &Permission{
"join_public_channels",
"authentication.permissions.join_public_channels.name",
"authentication.permissions.join_public_channels.description",
}
PERMISSION_DELETE_PUBLIC_CHANNEL = &Permission{
"delete_public_channel",
"authentication.permissions.delete_public_channel.name",
"authentication.permissions.delete_public_channel.description",
}
PERMISSION_DELETE_PRIVATE_CHANNEL = &Permission{
"delete_private_channel",
"authentication.permissions.delete_private_channel.name",
"authentication.permissions.delete_private_channel.description",
}
PERMISSION_EDIT_OTHER_USERS = &Permission{
"edit_other_users",
"authentication.permissions.edit_other_users.name",
"authentication.permissions.edit_other_users.description",
}
PERMISSION_READ_CHANNEL = &Permission{
"read_channel",
"authentication.permissions.read_channel.name",
"authentication.permissions.read_channel.description",
}
PERMISSION_PERMANENT_DELETE_USER = &Permission{
"permanent_delete_user",
"authentication.permissions.permanent_delete_user.name",
"authentication.permissions.permanent_delete_user.description",
}
PERMISSION_UPLOAD_FILE = &Permission{
"upload_file",
"authentication.permissions.upload_file.name",
"authentication.permissions.upload_file.description",
}
PERMISSION_GET_PUBLIC_LINK = &Permission{
"get_public_link",
"authentication.permissions.get_public_link.name",
"authentication.permissions.get_public_link.description",
}
PERMISSION_MANAGE_WEBHOOKS = &Permission{
"manage_webhooks",
"authentication.permissions.manage_webhooks.name",
"authentication.permissions.manage_webhooks.description",
}
PERMISSION_MANAGE_OTHERS_WEBHOOKS = &Permission{
"manage_others_webhooks",
"authentication.permissions.manage_others_webhooks.name",
"authentication.permissions.manage_others_webhooks.description",
}
PERMISSION_MANAGE_OAUTH = &Permission{
"manage_oauth",
"authentication.permissions.manage_oauth.name",
"authentication.permissions.manage_oauth.description",
}
PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH = &Permission{
"manage_sytem_wide_oauth",
"authentication.permissions.manage_sytem_wide_oauth.name",
"authentication.permissions.manage_sytem_wide_oauth.description",
}
PERMISSION_CREATE_POST = &Permission{
"create_post",
"authentication.permissions.create_post.name",
"authentication.permissions.create_post.description",
}
PERMISSION_EDIT_POST = &Permission{
"edit_post",
"authentication.permissions.edit_post.name",
"authentication.permissions.edit_post.description",
}
PERMISSION_EDIT_OTHERS_POSTS = &Permission{
"edit_others_posts",
"authentication.permissions.edit_others_posts.name",
"authentication.permissions.edit_others_posts.description",
}
PERMISSION_REMOVE_USER_FROM_TEAM = &Permission{
"remove_user_from_team",
"authentication.permissions.remove_user_from_team.name",
"authentication.permissions.remove_user_from_team.description",
}
PERMISSION_MANAGE_TEAM = &Permission{
"manage_team",
"authentication.permissions.manage_team.name",
"authentication.permissions.manage_team.description",
}
PERMISSION_IMPORT_TEAM = &Permission{
"import_team",
"authentication.permissions.import_team.name",
"authentication.permissions.import_team.description",
}
}
func InitalizeRoles() {
InitalizePermissions()
BuiltInRoles = make(map[string]*Role)
ROLE_CHANNEL_USER = &Role{
"channel_user",
"authentication.roles.channel_user.name",
"authentication.roles.channel_user.description",
[]string{
PERMISSION_READ_CHANNEL.Id,
PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id,
PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id,
PERMISSION_UPLOAD_FILE.Id,
PERMISSION_GET_PUBLIC_LINK.Id,
PERMISSION_CREATE_POST.Id,
PERMISSION_EDIT_POST.Id,
PERMISSION_USE_SLASH_COMMANDS.Id,
},
}
BuiltInRoles[ROLE_CHANNEL_USER.Id] = ROLE_CHANNEL_USER
ROLE_CHANNEL_ADMIN = &Role{
"channel_admin",
"authentication.roles.channel_admin.name",
"authentication.roles.channel_admin.description",
[]string{},
}
BuiltInRoles[ROLE_CHANNEL_ADMIN.Id] = ROLE_CHANNEL_ADMIN
ROLE_CHANNEL_GUEST = &Role{
"guest",
"authentication.roles.global_guest.name",
"authentication.roles.global_guest.description",
[]string{},
}
BuiltInRoles[ROLE_CHANNEL_GUEST.Id] = ROLE_CHANNEL_GUEST
ROLE_TEAM_USER = &Role{
"team_user",
"authentication.roles.team_user.name",
"authentication.roles.team_user.description",
[]string{
PERMISSION_LIST_TEAM_CHANNELS.Id,
PERMISSION_JOIN_PUBLIC_CHANNELS.Id,
},
}
BuiltInRoles[ROLE_TEAM_USER.Id] = ROLE_TEAM_USER
ROLE_TEAM_ADMIN = &Role{
"team_admin",
"authentication.roles.team_admin.name",
"authentication.roles.team_admin.description",
[]string{
PERMISSION_EDIT_OTHERS_POSTS.Id,
PERMISSION_ADD_USER_TO_TEAM.Id,
PERMISSION_REMOVE_USER_FROM_TEAM.Id,
PERMISSION_MANAGE_TEAM.Id,
PERMISSION_IMPORT_TEAM.Id,
PERMISSION_MANAGE_ROLES.Id,
PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id,
PERMISSION_MANAGE_SLASH_COMMANDS.Id,
PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS.Id,
PERMISSION_MANAGE_WEBHOOKS.Id,
},
}
BuiltInRoles[ROLE_TEAM_ADMIN.Id] = ROLE_TEAM_ADMIN
ROLE_SYSTEM_USER = &Role{
"system_user",
"authentication.roles.global_user.name",
"authentication.roles.global_user.description",
[]string{
PERMISSION_CREATE_DIRECT_CHANNEL.Id,
PERMISSION_PERMANENT_DELETE_USER.Id,
PERMISSION_MANAGE_OAUTH.Id,
},
}
BuiltInRoles[ROLE_SYSTEM_USER.Id] = ROLE_SYSTEM_USER
ROLE_SYSTEM_ADMIN = &Role{
"system_admin",
"authentication.roles.global_admin.name",
"authentication.roles.global_admin.description",
// System admins can do anything channel and team admins can do
// plus everything members of teams and channels can do to all teams
// and channels on the system
append(
append(
append(
append(
[]string{
PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE.Id,
PERMISSION_MANAGE_SYSTEM.Id,
PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES.Id,
PERMISSION_DELETE_PUBLIC_CHANNEL.Id,
PERMISSION_CREATE_PUBLIC_CHANNEL.Id,
PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES.Id,
PERMISSION_DELETE_PRIVATE_CHANNEL.Id,
PERMISSION_CREATE_PRIVATE_CHANNEL.Id,
PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH.Id,
PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id,
PERMISSION_EDIT_OTHER_USERS.Id,
PERMISSION_MANAGE_OAUTH.Id,
PERMISSION_INVITE_USER.Id,
},
ROLE_TEAM_USER.Permissions...,
),
ROLE_CHANNEL_USER.Permissions...,
),
ROLE_TEAM_ADMIN.Permissions...,
),
ROLE_CHANNEL_ADMIN.Permissions...,
),
}
BuiltInRoles[ROLE_SYSTEM_ADMIN.Id] = ROLE_SYSTEM_ADMIN
}
func RoleIdsToString(roles []string) string {
output := ""
for _, role := range roles {
output += role + ", "
}
if output == "" {
return "[<NO ROLES>]"
}
return output[:len(output)-1]
}
func init() {
InitalizeRoles()
}

View File

@ -0,0 +1,58 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type UserAutocompleteInChannel struct {
InChannel []*User `json:"in_channel"`
OutOfChannel []*User `json:"out_of_channel"`
}
type UserAutocompleteInTeam struct {
InTeam []*User `json:"in_team"`
}
func (o *UserAutocompleteInChannel) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func UserAutocompleteInChannelFromJson(data io.Reader) *UserAutocompleteInChannel {
decoder := json.NewDecoder(data)
var o UserAutocompleteInChannel
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}
func (o *UserAutocompleteInTeam) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func UserAutocompleteInTeamFromJson(data io.Reader) *UserAutocompleteInTeam {
decoder := json.NewDecoder(data)
var o UserAutocompleteInTeam
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -14,6 +14,10 @@ const (
CHANNEL_PRIVATE = "P"
CHANNEL_DIRECT = "D"
DEFAULT_CHANNEL = "town-square"
CHANNEL_DISPLAY_NAME_MAX_RUNES = 64
CHANNEL_NAME_MAX_LENGTH = 64
CHANNEL_HEADER_MAX_RUNES = 1024
CHANNEL_PURPOSE_MAX_RUNES = 250
)
type Channel struct {
@ -57,8 +61,8 @@ func (o *Channel) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
func (o *Channel) ExtraEtag(memberLimit int) string {
return Etag(o.Id, o.ExtraUpdateAt, memberLimit)
func (o *Channel) StatsEtag() string {
return Etag(o.Id, o.ExtraUpdateAt)
}
func (o *Channel) IsValid() *AppError {
@ -75,11 +79,11 @@ func (o *Channel) IsValid() *AppError {
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.update_at.app_error", nil, "id="+o.Id)
}
if utf8.RuneCountInString(o.DisplayName) > 64 {
if utf8.RuneCountInString(o.DisplayName) > CHANNEL_DISPLAY_NAME_MAX_RUNES {
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.display_name.app_error", nil, "id="+o.Id)
}
if len(o.Name) > 64 {
if len(o.Name) > CHANNEL_NAME_MAX_LENGTH {
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.name.app_error", nil, "id="+o.Id)
}
@ -91,11 +95,11 @@ func (o *Channel) IsValid() *AppError {
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.type.app_error", nil, "id="+o.Id)
}
if utf8.RuneCountInString(o.Header) > 1024 {
if utf8.RuneCountInString(o.Header) > CHANNEL_HEADER_MAX_RUNES {
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.header.app_error", nil, "id="+o.Id)
}
if utf8.RuneCountInString(o.Purpose) > 128 {
if utf8.RuneCountInString(o.Purpose) > CHANNEL_PURPOSE_MAX_RUNES {
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.purpose.app_error", nil, "id="+o.Id)
}

View File

@ -1,49 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type ExtraMember struct {
Id string `json:"id"`
Nickname string `json:"nickname"`
Email string `json:"email"`
Roles string `json:"roles"`
Username string `json:"username"`
}
func (o *ExtraMember) Sanitize(options map[string]bool) {
if len(options) == 0 || !options["email"] {
o.Email = ""
}
}
type ChannelExtra struct {
Id string `json:"id"`
Members []ExtraMember `json:"members"`
MemberCount int64 `json:"member_count"`
}
func (o *ChannelExtra) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func ChannelExtraFromJson(data io.Reader) *ChannelExtra {
decoder := json.NewDecoder(data)
var o ChannelExtra
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -8,15 +8,11 @@ import (
"io"
)
type ChannelList struct {
Channels []*Channel `json:"channels"`
Members map[string]*ChannelMember `json:"members"`
}
type ChannelList []*Channel
func (o *ChannelList) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
if b, err := json.Marshal(o); err != nil {
return "[]"
} else {
return string(b)
}
@ -28,7 +24,7 @@ func (o *ChannelList) Etag() string {
var t int64 = 0
var delta int64 = 0
for _, v := range o.Channels {
for _, v := range *o {
if v.LastPostAt > t {
t = v.LastPostAt
id = v.Id
@ -39,30 +35,9 @@ func (o *ChannelList) Etag() string {
id = v.Id
}
member := o.Members[v.Id]
if member != nil {
max := v.LastPostAt
if v.UpdateAt > max {
max = v.UpdateAt
}
delta += max - member.LastViewedAt
if member.LastViewedAt > t {
t = member.LastViewedAt
id = v.Id
}
if member.LastUpdateAt > t {
t = member.LastUpdateAt
id = v.Id
}
}
}
return Etag(id, t, delta, len(o.Channels))
return Etag(id, t, delta, len(*o))
}
func ChannelListFromJson(data io.Reader) *ChannelList {

View File

@ -10,7 +10,6 @@ import (
)
const (
CHANNEL_ROLE_ADMIN = "admin"
CHANNEL_NOTIFY_DEFAULT = "default"
CHANNEL_NOTIFY_ALL = "all"
CHANNEL_NOTIFY_MENTION = "mention"
@ -30,6 +29,27 @@ type ChannelMember struct {
LastUpdateAt int64 `json:"last_update_at"`
}
type ChannelMembers []ChannelMember
func (o *ChannelMembers) ToJson() string {
if b, err := json.Marshal(o); err != nil {
return "[]"
} else {
return string(b)
}
}
func ChannelMembersFromJson(data io.Reader) *ChannelMembers {
decoder := json.NewDecoder(data)
var o ChannelMembers
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}
func (o *ChannelMember) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
@ -60,12 +80,6 @@ func (o *ChannelMember) IsValid() *AppError {
return NewLocAppError("ChannelMember.IsValid", "model.channel_member.is_valid.user_id.app_error", nil, "")
}
for _, role := range strings.Split(o.Roles, " ") {
if !(role == "" || role == CHANNEL_ROLE_ADMIN) {
return NewLocAppError("ChannelMember.IsValid", "model.channel_member.is_valid.role.app_error", nil, "role="+role)
}
}
notifyLevel := o.NotifyProps["desktop"]
if len(notifyLevel) > 20 || !IsChannelNotifyLevelValid(notifyLevel) {
return NewLocAppError("ChannelMember.IsValid", "model.channel_member.is_valid.notify_level.app_error",
@ -89,6 +103,10 @@ func (o *ChannelMember) PreUpdate() {
o.LastUpdateAt = GetMillis()
}
func (o *ChannelMember) GetRoles() []string {
return strings.Fields(o.Roles)
}
func IsChannelNotifyLevelValid(notifyLevel string) bool {
return notifyLevel == CHANNEL_NOTIFY_DEFAULT ||
notifyLevel == CHANNEL_NOTIFY_ALL ||

View File

@ -0,0 +1,34 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type ChannelStats struct {
ChannelId string `json:"channel_id"`
MemberCount int64 `json:"member_count"`
}
func (o *ChannelStats) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func ChannelStatsFromJson(data io.Reader) *ChannelStats {
decoder := json.NewDecoder(data)
var o ChannelStats
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -108,6 +108,10 @@ func (c *Client) GetChannelRoute(channelId string) string {
return fmt.Sprintf("/teams/%v/channels/%v", c.GetTeamId(), channelId)
}
func (c *Client) GetUserRequiredRoute(userId string) string {
return fmt.Sprintf("/users/%v", userId)
}
func (c *Client) GetChannelNameRoute(channelName string) string {
return fmt.Sprintf("/teams/%v/channels/name/%v", c.GetTeamId(), channelName)
}
@ -120,9 +124,14 @@ func (c *Client) GetGeneralRoute() string {
return "/general"
}
func (c *Client) GetFileRoute(fileId string) string {
return fmt.Sprintf("/files/%v", fileId)
}
func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppError) {
rq, _ := http.NewRequest("POST", c.Url+url, strings.NewReader(data))
rq.Header.Set("Content-Type", contentType)
rq.Close = true
if rp, err := c.HttpClient.Do(rq); err != nil {
return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error())
@ -136,6 +145,7 @@ func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppErro
func (c *Client) DoApiPost(url string, data string) (*http.Response, *AppError) {
rq, _ := http.NewRequest("POST", c.ApiUrl+url, strings.NewReader(data))
rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
@ -153,6 +163,7 @@ func (c *Client) DoApiPost(url string, data string) (*http.Response, *AppError)
func (c *Client) DoApiGet(url string, data string, etag string) (*http.Response, *AppError) {
rq, _ := http.NewRequest("GET", c.ApiUrl+url, strings.NewReader(data))
rq.Close = true
if len(etag) > 0 {
rq.Header.Set(HEADER_ETAG_CLIENT, etag)
@ -500,10 +511,9 @@ 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 {
// GetProfiles returns a map of users using user id as the key. Must be authenticated.
func (c *Client) GetProfiles(offset int, limit int, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf("/users/%v/%v", offset, limit), "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -512,10 +522,10 @@ func (c *Client) GetProfilesForDirectMessageList(teamId string) (*Result, *AppEr
}
}
// GetProfiles returns a map of users for a team using user id as the key. Must
// GetProfilesInTeam 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 {
func (c *Client) GetProfilesInTeam(teamId string, offset int, limit int, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/users/%v/%v", teamId, offset, limit), "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -524,10 +534,10 @@ 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 {
// GetProfilesInChannel returns a map of users for a channel using user id as the key. Must
// be authenticated.
func (c *Client) GetProfilesInChannel(channelId string, offset int, limit int, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf(c.GetChannelRoute(channelId)+"/users/%v/%v", offset, limit), "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -536,6 +546,70 @@ func (c *Client) GetDirectProfiles(etag string) (*Result, *AppError) {
}
}
// GetProfilesNotInChannel returns a map of users not in a channel but on the team using user id as the key. Must
// be authenticated.
func (c *Client) GetProfilesNotInChannel(channelId string, offset int, limit int, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf(c.GetChannelRoute(channelId)+"/users/not_in_channel/%v/%v", offset, limit), "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil
}
}
// GetProfilesByIds returns a map of users based on the user ids provided. Must
// be authenticated.
func (c *Client) GetProfilesByIds(userIds []string) (*Result, *AppError) {
if r, err := c.DoApiPost("/users/ids", ArrayToJson(userIds)); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil
}
}
// SearchUsers returns a list of users that have a username matching or similar to the search term. Must
// be authenticated.
func (c *Client) SearchUsers(params UserSearch) (*Result, *AppError) {
if r, err := c.DoApiPost("/users/search", params.ToJson()); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), UserListFromJson(r.Body)}, nil
}
}
// AutocompleteUsersInChannel returns two lists for autocompletion of users in a channel. The first list "in_channel",
// specifies users in the channel. The second list "out_of_channel" specifies users outside of the
// channel. Term, the string to search against, is required, channel id is also required. Must be authenticated.
func (c *Client) AutocompleteUsersInChannel(term string, channelId string) (*Result, *AppError) {
url := fmt.Sprintf("%s/users/autocomplete?term=%s", c.GetChannelRoute(channelId), url.QueryEscape(term))
if r, err := c.DoApiGet(url, "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), UserAutocompleteInChannelFromJson(r.Body)}, nil
}
}
// AutocompleteUsersInTeam returns a list for autocompletion of users in a team. The list "in_team" specifies
// the users in the team that match the provided term, matching against username, full name and
// nickname. Must be authenticated.
func (c *Client) AutocompleteUsersInTeam(term string) (*Result, *AppError) {
url := fmt.Sprintf("%s/users/autocomplete?term=%s", c.GetTeamRoute(), url.QueryEscape(term))
if r, err := c.DoApiGet(url, "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), UserAutocompleteInTeamFromJson(r.Body)}, nil
}
}
// LoginById authenticates a user by user id and password.
func (c *Client) LoginById(id string, password string) (*Result, *AppError) {
m := make(map[string]string)
@ -622,15 +696,16 @@ 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 {
// GenerateMfaSecret returns a QR code image containing the secret, to be scanned
// by a multi-factor authentication mobile application. It also returns the secret
// for manual entry. Must be authenticated.
func (c *Client) GenerateMfaSecret() (*Result, *AppError) {
if r, err := c.DoApiGet("/users/generate_mfa_secret", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), r.Body}, nil
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
@ -934,6 +1009,7 @@ func (c *Client) SaveComplianceReport(job *Compliance) (*Result, *AppError) {
func (c *Client) DownloadComplianceReport(id string) (*Result, *AppError) {
var rq *http.Request
rq, _ = http.NewRequest("GET", c.ApiUrl+"/admin/download_compliance_report/"+id, nil)
rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
@ -1047,13 +1123,13 @@ func (c *Client) UpdateNotifyProps(data map[string]string) (*Result, *AppError)
}
}
func (c *Client) GetChannels(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/", "", etag); err != nil {
func (c *Client) GetMyChannelMembers() (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/members", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil
r.Header.Get(HEADER_ETAG_SERVER), ChannelMembersFromJson(r.Body)}, nil
}
}
@ -1087,6 +1163,16 @@ func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) {
}
}
func (c *Client) GetChannels(etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/", "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil
}
}
func (c *Client) JoinChannel(id string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/join", ""); err != nil {
return nil, err
@ -1166,13 +1252,23 @@ func (c *Client) UpdateLastViewedAt(channelId string, active bool) (*Result, *Ap
}
}
func (c *Client) GetChannelExtraInfo(id string, memberLimit int, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetChannelRoute(id)+"/extra_info/"+strconv.FormatInt(int64(memberLimit), 10), "", etag); err != nil {
func (c *Client) GetChannelStats(id string, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetChannelRoute(id)+"/stats", "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelExtraFromJson(r.Body)}, nil
r.Header.Get(HEADER_ETAG_SERVER), ChannelStatsFromJson(r.Body)}, nil
}
}
func (c *Client) GetChannelMember(channelId string, userId string) (*Result, *AppError) {
if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/members/"+userId, "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelMemberFromJson(r.Body)}, nil
}
}
@ -1285,13 +1381,39 @@ func (c *Client) UploadProfileFile(data []byte, contentType string) (*Result, *A
return c.uploadFile(c.ApiUrl+"/users/newimage", data, contentType)
}
func (c *Client) UploadPostAttachment(data []byte, contentType string) (*Result, *AppError) {
return c.uploadFile(c.ApiUrl+c.GetTeamRoute()+"/files/upload", data, contentType)
func (c *Client) UploadPostAttachment(data []byte, channelId string, filename string) (*FileUploadResponse, *AppError) {
c.clearExtraProperties()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
if part, err := writer.CreateFormFile("files", filename); err != nil {
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.file.app_error", nil, err.Error())
} else if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.file.app_error", nil, err.Error())
}
if part, err := writer.CreateFormField("channel_id"); err != nil {
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.channel_id.app_error", nil, err.Error())
} else if _, err = io.Copy(part, strings.NewReader(channelId)); err != nil {
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.channel_id.app_error", nil, err.Error())
}
if err := writer.Close(); err != nil {
return nil, NewLocAppError("UploadPostAttachment", "model.client.upload_post_attachment.writer.app_error", nil, err.Error())
}
if result, err := c.uploadFile(c.ApiUrl+c.GetTeamRoute()+"/files/upload", body.Bytes(), writer.FormDataContentType()); err != nil {
return nil, err
} else {
return result.Data.(*FileUploadResponse), nil
}
}
func (c *Client) uploadFile(url string, data []byte, contentType string) (*Result, *AppError) {
rq, _ := http.NewRequest("POST", url, bytes.NewReader(data))
rq.Header.Set("Content-Type", contentType)
rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
@ -1308,55 +1430,51 @@ func (c *Client) uploadFile(url string, data []byte, contentType string) (*Resul
}
}
func (c *Client) GetFile(url string, isFullUrl bool) (*Result, *AppError) {
var rq *http.Request
if isFullUrl {
rq, _ = http.NewRequest("GET", url, nil)
func (c *Client) GetFile(fileId string) (io.ReadCloser, *AppError) {
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get", "", ""); err != nil {
return nil, err
} else {
rq, _ = http.NewRequest("GET", c.ApiUrl+c.GetTeamRoute()+"/files/get"+url, nil)
c.fillInExtraProperties(r)
return r.Body, nil
}
}
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
}
if rp, err := c.HttpClient.Do(rq); err != nil {
return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error())
} else if rp.StatusCode >= 300 {
return nil, AppErrorFromJson(rp.Body)
func (c *Client) GetFileThumbnail(fileId string) (io.ReadCloser, *AppError) {
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_thumbnail", "", ""); err != nil {
return nil, err
} else {
defer closeBody(rp)
return &Result{rp.Header.Get(HEADER_REQUEST_ID),
rp.Header.Get(HEADER_ETAG_SERVER), rp.Body}, nil
c.fillInExtraProperties(r)
return r.Body, nil
}
}
func (c *Client) GetFileInfo(url string) (*Result, *AppError) {
var rq *http.Request
rq, _ = http.NewRequest("GET", c.ApiUrl+c.GetTeamRoute()+"/files/get_info"+url, nil)
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
}
if rp, err := c.HttpClient.Do(rq); err != nil {
return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error())
} else if rp.StatusCode >= 300 {
return nil, AppErrorFromJson(rp.Body)
} else {
defer closeBody(rp)
return &Result{rp.Header.Get(HEADER_REQUEST_ID),
rp.Header.Get(HEADER_ETAG_SERVER), FileInfoFromJson(rp.Body)}, nil
}
}
func (c *Client) GetPublicLink(filename string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetTeamRoute()+"/files/get_public_link", MapToJson(map[string]string{"filename": filename})); err != nil {
func (c *Client) GetFilePreview(fileId string) (io.ReadCloser, *AppError) {
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_preview", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), StringFromJson(r.Body)}, nil
c.fillInExtraProperties(r)
return r.Body, nil
}
}
func (c *Client) GetFileInfo(fileId string) (*FileInfo, *AppError) {
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_info", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
c.fillInExtraProperties(r)
return FileInfoFromJson(r.Body), nil
}
}
func (c *Client) GetPublicLink(fileId string) (string, *AppError) {
if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_public_link", "", ""); err != nil {
return "", err
} else {
defer closeBody(r)
c.fillInExtraProperties(r)
return StringFromJson(r.Body), nil
}
}
@ -1370,8 +1488,25 @@ func (c *Client) UpdateUser(user *User) (*Result, *AppError) {
}
}
func (c *Client) UpdateUserRoles(data map[string]string) (*Result, *AppError) {
if r, err := c.DoApiPost("/users/update_roles", MapToJson(data)); err != nil {
func (c *Client) UpdateUserRoles(userId string, roles string) (*Result, *AppError) {
data := make(map[string]string)
data["new_roles"] = roles
if r, err := c.DoApiPost(c.GetUserRequiredRoute(userId)+"/update_roles", 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) UpdateTeamRoles(userId string, roles string) (*Result, *AppError) {
data := make(map[string]string)
data["new_roles"] = roles
data["user_id"] = userId
if r, err := c.DoApiPost(c.GetTeamRoute()+"/update_member_roles", MapToJson(data)); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -1479,6 +1614,18 @@ func (c *Client) GetStatuses() (*Result, *AppError) {
}
}
// GetStatusesByIds returns a map of string statuses using user id as the key,
// based on the provided user ids
func (c *Client) GetStatusesByIds(userIds []string) (*Result, *AppError) {
if r, err := c.DoApiPost("/users/status/ids", ArrayToJson(userIds)); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
// SetActiveChannel sets the the channel id the user is currently viewing.
// The channelId key is required but the value can be blank. Returns standard
// response.
@ -1504,8 +1651,46 @@ func (c *Client) GetMyTeam(etag string) (*Result, *AppError) {
}
}
func (c *Client) GetTeamMembers(teamId string) (*Result, *AppError) {
if r, err := c.DoApiGet("/teams/members/"+teamId, "", ""); err != nil {
// GetTeamMembers will return a page of team member objects as an array paged based on the
// team id, offset and limit provided. Must be authenticated.
func (c *Client) GetTeamMembers(teamId string, offset int, limit int) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/members/%v/%v", teamId, 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), TeamMembersFromJson(r.Body)}, nil
}
}
// GetTeamMember will return a team member object based on the team id and user id provided.
// Must be authenticated.
func (c *Client) GetTeamMember(teamId string, userId string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/members/%v", teamId, userId), "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), TeamMemberFromJson(r.Body)}, nil
}
}
// GetTeamStats will return a team stats object containing the number of users on the team
// based on the team id provided. Must be authenticated.
func (c *Client) GetTeamStats(teamId string) (*Result, *AppError) {
if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/stats", teamId), "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), TeamStatsFromJson(r.Body)}, nil
}
}
// GetTeamMembersByIds will return team member objects as an array based on the
// team id and a list of user ids provided. Must be authenticated.
func (c *Client) GetTeamMembersByIds(teamId string, userIds []string) (*Result, *AppError) {
if r, err := c.DoApiPost(fmt.Sprintf("/teams/%v/members/ids", teamId), ArrayToJson(userIds)); err != nil {
return nil, err
} else {
defer closeBody(r)
@ -1820,6 +2005,7 @@ func (c *Client) CreateEmoji(emoji *Emoji, image []byte, filename string) (*Emoj
rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetEmojiRoute()+"/create", body)
rq.Header.Set("Content-Type", writer.FormDataContentType())
rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
@ -1862,6 +2048,7 @@ func (c *Client) UploadCertificateFile(data []byte, contentType string) *AppErro
url := c.ApiUrl + "/admin/add_certificate"
rq, _ := http.NewRequest("POST", url, bytes.NewReader(data))
rq.Header.Set("Content-Type", contentType)
rq.Close = true
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
@ -1898,3 +2085,28 @@ func (c *Client) SamlCertificateStatus(filename string) (map[string]interface{},
return StringInterfaceFromJson(r.Body), nil
}
}
// GetWebrtcToken if Successful returns a map with a valid token, stun server and turn server with credentials to use with
// the Mattermost WebRTC service, otherwise returns an AppError. Must be authenticated user.
func (c *Client) GetWebrtcToken() (map[string]string, *AppError) {
if r, err := c.DoApiPost("/webrtc/token", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return MapFromJson(r.Body), nil
}
}
// GetFileInfosForPost returns a list of FileInfo objects for a given post id, if successful.
// Otherwise, it returns an error.
func (c *Client) GetFileInfosForPost(channelId string, postId string, etag string) ([]*FileInfo, *AppError) {
c.clearExtraProperties()
if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/get_file_infos", postId), "", etag); err != nil {
return nil, err
} else {
defer closeBody(r)
c.fillInExtraProperties(r)
return FileInfosFromJson(r.Body), nil
}
}

View File

@ -34,7 +34,7 @@ type CompliancePost struct {
PostType string
PostProps string
PostHashtags string
PostFilenames string
PostFileIds string
}
func CompliancePostHeader() []string {
@ -60,7 +60,7 @@ func CompliancePostHeader() []string {
"PostType",
"PostProps",
"PostHashtags",
"PostFilenames",
"PostFileIds",
}
}
@ -99,6 +99,6 @@ func (me *CompliancePost) Row() []string {
me.PostType,
me.PostProps,
me.PostHashtags,
me.PostFilenames,
me.PostFileIds,
}
}

View File

@ -57,6 +57,14 @@ const (
type ServiceSettings struct {
SiteURL *string
ListenAddress string
ConnectionSecurity *string
TLSCertFile *string
TLSKeyFile *string
UseLetsEncrypt *bool
LetsEncryptCertificateCacheFile *string
Forward80To443 *bool
ReadTimeout *int
WriteTimeout *int
MaximumLoginAttempts int
SegmentDeveloperKey string
GoogleDeveloperKey string
@ -147,9 +155,7 @@ type FileSettings struct {
AmazonS3Bucket string
AmazonS3Region string
AmazonS3Endpoint string
AmazonS3BucketEndpoint string
AmazonS3LocationConstraint *bool
AmazonS3LowercaseBucket *bool
AmazonS3SSL *bool
}
type EmailSettings struct {
@ -177,8 +183,9 @@ type EmailSettings struct {
}
type RateLimitSettings struct {
EnableRateLimiter bool
Enable *bool
PerSec int
MaxBurst *int
MemoryStoreSize int
VaryByRemoteAddr bool
VaryByHeader string
@ -205,7 +212,6 @@ type TeamSettings struct {
EnableUserCreation bool
EnableOpenServer *bool
RestrictCreationToDomains string
RestrictTeamNames *bool
EnableCustomBrand *bool
CustomBrandText *string
CustomDescriptionText *string
@ -214,6 +220,7 @@ type TeamSettings struct {
RestrictPublicChannelManagement *string
RestrictPrivateChannelManagement *string
UserStatusAwayTimeout *int64
MaxChannelsPerTeam *int64
}
type LdapSettings struct {
@ -292,6 +299,17 @@ type NativeAppSettings struct {
IosAppDownloadLink *string
}
type WebrtcSettings struct {
Enable *bool
GatewayWebsocketUrl *string
GatewayAdminUrl *string
GatewayAdminSecret *string
StunURI *string
TurnURI *string
TurnUsername *string
TurnSharedKey *string
}
type Config struct {
ServiceSettings ServiceSettings
TeamSettings TeamSettings
@ -312,6 +330,7 @@ type Config struct {
SamlSettings SamlSettings
NativeAppSettings NativeAppSettings
ClusterSettings ClusterSettings
WebrtcSettings WebrtcSettings
}
func (o *Config) ToJson() string {
@ -353,26 +372,27 @@ func (o *Config) SetDefaults() {
o.SqlSettings.AtRestEncryptKey = NewRandomString(32)
}
if o.FileSettings.AmazonS3Endpoint == "" {
// Defaults to "s3.amazonaws.com"
o.FileSettings.AmazonS3Endpoint = "s3.amazonaws.com"
}
if o.FileSettings.AmazonS3Region == "" {
// Defaults to "us-east-1" region.
o.FileSettings.AmazonS3Region = "us-east-1"
}
if o.FileSettings.AmazonS3SSL == nil {
o.FileSettings.AmazonS3SSL = new(bool)
*o.FileSettings.AmazonS3SSL = true // Secure by default.
}
if o.FileSettings.MaxFileSize == nil {
o.FileSettings.MaxFileSize = new(int64)
*o.FileSettings.MaxFileSize = 52428800 // 50 MB
}
if len(*o.FileSettings.PublicLinkSalt) == 0 {
o.FileSettings.PublicLinkSalt = new(string)
*o.FileSettings.PublicLinkSalt = NewRandomString(32)
}
if o.FileSettings.AmazonS3LocationConstraint == nil {
o.FileSettings.AmazonS3LocationConstraint = new(bool)
*o.FileSettings.AmazonS3LocationConstraint = false
}
if o.FileSettings.AmazonS3LowercaseBucket == nil {
o.FileSettings.AmazonS3LowercaseBucket = new(bool)
*o.FileSettings.AmazonS3LowercaseBucket = false
}
if len(o.EmailSettings.InviteSalt) == 0 {
o.EmailSettings.InviteSalt = NewRandomString(32)
}
@ -431,11 +451,6 @@ func (o *Config) SetDefaults() {
*o.PasswordSettings.Symbol = false
}
if o.TeamSettings.RestrictTeamNames == nil {
o.TeamSettings.RestrictTeamNames = new(bool)
*o.TeamSettings.RestrictTeamNames = true
}
if o.TeamSettings.EnableCustomBrand == nil {
o.TeamSettings.EnableCustomBrand = new(bool)
*o.TeamSettings.EnableCustomBrand = false
@ -481,6 +496,11 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.UserStatusAwayTimeout = 300
}
if o.TeamSettings.MaxChannelsPerTeam == nil {
o.TeamSettings.MaxChannelsPerTeam = new(int64)
*o.TeamSettings.MaxChannelsPerTeam = 2000
}
if o.EmailSettings.EnableSignInWithEmail == nil {
o.EmailSettings.EnableSignInWithEmail = new(bool)
@ -881,6 +901,58 @@ func (o *Config) SetDefaults() {
o.NativeAppSettings.IosAppDownloadLink = new(string)
*o.NativeAppSettings.IosAppDownloadLink = "https://about.mattermost.com/mattermost-ios-app/"
}
if o.RateLimitSettings.Enable == nil {
o.RateLimitSettings.Enable = new(bool)
*o.RateLimitSettings.Enable = false
}
if o.RateLimitSettings.MaxBurst == nil {
o.RateLimitSettings.MaxBurst = new(int)
*o.RateLimitSettings.MaxBurst = 100
}
if o.ServiceSettings.ConnectionSecurity == nil {
o.ServiceSettings.ConnectionSecurity = new(string)
*o.ServiceSettings.ConnectionSecurity = ""
}
if o.ServiceSettings.TLSKeyFile == nil {
o.ServiceSettings.TLSKeyFile = new(string)
*o.ServiceSettings.TLSKeyFile = ""
}
if o.ServiceSettings.TLSCertFile == nil {
o.ServiceSettings.TLSCertFile = new(string)
*o.ServiceSettings.TLSCertFile = ""
}
if o.ServiceSettings.UseLetsEncrypt == nil {
o.ServiceSettings.UseLetsEncrypt = new(bool)
*o.ServiceSettings.UseLetsEncrypt = false
}
if o.ServiceSettings.LetsEncryptCertificateCacheFile == nil {
o.ServiceSettings.LetsEncryptCertificateCacheFile = new(string)
*o.ServiceSettings.LetsEncryptCertificateCacheFile = "./config/letsencrypt.cache"
}
if o.ServiceSettings.ReadTimeout == nil {
o.ServiceSettings.ReadTimeout = new(int)
*o.ServiceSettings.ReadTimeout = 300
}
if o.ServiceSettings.WriteTimeout == nil {
o.ServiceSettings.WriteTimeout = new(int)
*o.ServiceSettings.WriteTimeout = 300
}
if o.ServiceSettings.Forward80To443 == nil {
o.ServiceSettings.Forward80To443 = new(bool)
*o.ServiceSettings.Forward80To443 = false
}
o.defaultWebrtcSettings()
}
func (o *Config) IsValid() *AppError {
@ -911,6 +983,10 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.max_users.app_error", nil, "")
}
if *o.TeamSettings.MaxChannelsPerTeam <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.max_channels.app_error", nil, "")
}
if !(*o.TeamSettings.RestrictDirectMessage == DIRECT_MESSAGE_ANY || *o.TeamSettings.RestrictDirectMessage == DIRECT_MESSAGE_TEAM) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.restrict_direct_message.app_error", nil, "")
}
@ -1083,6 +1159,26 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.sitename_length.app_error", map[string]interface{}{"MaxLength": SITENAME_MAX_LENGTH}, "")
}
if *o.RateLimitSettings.MaxBurst <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.max_burst.app_error", nil, "")
}
if err := o.isValidWebrtcSettings(); err != nil {
return err
}
if !(*o.ServiceSettings.ConnectionSecurity == CONN_SECURITY_NONE || *o.ServiceSettings.ConnectionSecurity == CONN_SECURITY_TLS) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webserver_security.app_error", nil, "")
}
if *o.ServiceSettings.ReadTimeout <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.read_timeout.app_error", nil, "")
}
if *o.ServiceSettings.WriteTimeout <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.write_timeout.app_error", nil, "")
}
return nil
}
@ -1121,3 +1217,71 @@ func (o *Config) Sanitize() {
o.SqlSettings.DataSourceReplicas[i] = FAKE_SETTING
}
}
func (o *Config) defaultWebrtcSettings() {
if o.WebrtcSettings.Enable == nil {
o.WebrtcSettings.Enable = new(bool)
*o.WebrtcSettings.Enable = false
}
if o.WebrtcSettings.GatewayWebsocketUrl == nil {
o.WebrtcSettings.GatewayWebsocketUrl = new(string)
*o.WebrtcSettings.GatewayWebsocketUrl = ""
}
if o.WebrtcSettings.GatewayAdminUrl == nil {
o.WebrtcSettings.GatewayAdminUrl = new(string)
*o.WebrtcSettings.GatewayAdminUrl = ""
}
if o.WebrtcSettings.GatewayAdminSecret == nil {
o.WebrtcSettings.GatewayAdminSecret = new(string)
*o.WebrtcSettings.GatewayAdminSecret = ""
}
if o.WebrtcSettings.StunURI == nil {
o.WebrtcSettings.StunURI = new(string)
*o.WebrtcSettings.StunURI = ""
}
if o.WebrtcSettings.TurnURI == nil {
o.WebrtcSettings.TurnURI = new(string)
*o.WebrtcSettings.TurnURI = ""
}
if o.WebrtcSettings.TurnUsername == nil {
o.WebrtcSettings.TurnUsername = new(string)
*o.WebrtcSettings.TurnUsername = ""
}
if o.WebrtcSettings.TurnSharedKey == nil {
o.WebrtcSettings.TurnSharedKey = new(string)
*o.WebrtcSettings.TurnSharedKey = ""
}
}
func (o *Config) isValidWebrtcSettings() *AppError {
if *o.WebrtcSettings.Enable {
if len(*o.WebrtcSettings.GatewayWebsocketUrl) == 0 || !IsValidWebsocketUrl(*o.WebrtcSettings.GatewayWebsocketUrl) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webrtc_gateway_ws_url.app_error", nil, "")
} else if len(*o.WebrtcSettings.GatewayAdminUrl) == 0 || !IsValidHttpUrl(*o.WebrtcSettings.GatewayAdminUrl) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webrtc_gateway_admin_url.app_error", nil, "")
} else if len(*o.WebrtcSettings.GatewayAdminSecret) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webrtc_gateway_admin_secret.app_error", nil, "")
} else if len(*o.WebrtcSettings.StunURI) != 0 && !IsValidTurnOrStunServer(*o.WebrtcSettings.StunURI) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webrtc_stun_uri.app_error", nil, "")
} else if len(*o.WebrtcSettings.TurnURI) != 0 {
if !IsValidTurnOrStunServer(*o.WebrtcSettings.TurnURI) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webrtc_turn_uri.app_error", nil, "")
}
if len(*o.WebrtcSettings.TurnUsername) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webrtc_turn_username.app_error", nil, "")
} else if len(*o.WebrtcSettings.TurnSharedKey) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.webrtc_turn_shared_key.app_error", nil, "")
}
}
}
return nil
}

View File

@ -14,7 +14,7 @@ var (
)
type FileUploadResponse struct {
Filenames []string `json:"filenames"`
FileInfos []*FileInfo `json:"file_infos"`
ClientIds []string `json:"client_ids"`
}

View File

@ -6,54 +6,31 @@ package model
import (
"bytes"
"encoding/json"
"image"
"image/gif"
"io"
"mime"
"path/filepath"
"strings"
)
type FileInfo struct {
Filename string `json:"filename"`
Size int `json:"size"`
Id string `json:"id"`
CreatorId string `json:"user_id"`
PostId string `json:"post_id,omitempty"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
Path string `json:"-"` // not sent back to the client
ThumbnailPath string `json:"-"` // not sent back to the client
PreviewPath string `json:"-"` // not sent back to the client
Name string `json:"name"`
Extension string `json:"extension"`
Size int64 `json:"size"`
MimeType string `json:"mime_type"`
HasPreviewImage bool `json:"has_preview_image"`
}
func GetInfoForBytes(filename string, data []byte) (*FileInfo, *AppError) {
size := len(data)
var mimeType string
extension := filepath.Ext(filename)
isImage := IsFileExtImage(extension)
if isImage {
mimeType = GetImageMimeType(extension)
} else {
mimeType = mime.TypeByExtension(extension)
}
if extension != "" && extension[0] == '.' {
// the client expects a file extension without the leading period
extension = extension[1:]
}
hasPreviewImage := isImage
if mimeType == "image/gif" {
// just show the gif itself instead of a preview image for animated gifs
if gifImage, err := gif.DecodeAll(bytes.NewReader(data)); err != nil {
return nil, NewLocAppError("GetInfoForBytes", "model.file_info.get.gif.app_error", nil, "filename="+filename)
} else {
hasPreviewImage = len(gifImage.Image) == 1
}
}
return &FileInfo{
Filename: filename,
Size: size,
Extension: extension,
MimeType: mimeType,
HasPreviewImage: hasPreviewImage,
}, nil
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
HasPreviewImage bool `json:"has_preview_image,omitempty"`
}
func (info *FileInfo) ToJson() string {
@ -75,3 +52,123 @@ func FileInfoFromJson(data io.Reader) *FileInfo {
return &info
}
}
func FileInfosToJson(infos []*FileInfo) string {
b, err := json.Marshal(infos)
if err != nil {
return ""
} else {
return string(b)
}
}
func FileInfosFromJson(data io.Reader) []*FileInfo {
decoder := json.NewDecoder(data)
var infos []*FileInfo
if err := decoder.Decode(&infos); err != nil {
return nil
} else {
return infos
}
}
func (o *FileInfo) PreSave() {
if o.Id == "" {
o.Id = NewId()
}
if o.CreateAt == 0 {
o.CreateAt = GetMillis()
o.UpdateAt = o.CreateAt
}
}
func (o *FileInfo) IsValid() *AppError {
if len(o.Id) != 26 {
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.id.app_error", nil, "")
}
if len(o.CreatorId) != 26 {
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.user_id.app_error", nil, "id="+o.Id)
}
if len(o.PostId) != 0 && len(o.PostId) != 26 {
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.post_id.app_error", nil, "id="+o.Id)
}
if o.CreateAt == 0 {
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.create_at.app_error", nil, "id="+o.Id)
}
if o.UpdateAt == 0 {
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.update_at.app_error", nil, "id="+o.Id)
}
if o.Path == "" {
return NewLocAppError("FileInfo.IsValid", "model.file_info.is_valid.path.app_error", nil, "id="+o.Id)
}
return nil
}
func (o *FileInfo) IsImage() bool {
return strings.HasPrefix(o.MimeType, "image")
}
func GetInfoForBytes(name string, data []byte) (*FileInfo, *AppError) {
info := &FileInfo{
Name: name,
Size: int64(len(data)),
}
var err *AppError
extension := strings.ToLower(filepath.Ext(name))
info.MimeType = mime.TypeByExtension(extension)
if extension != "" && extension[0] == '.' {
// The client expects a file extension without the leading period
info.Extension = extension[1:]
} else {
info.Extension = extension
}
if info.IsImage() {
// Only set the width and height if it's actually an image that we can understand
if config, _, err := image.DecodeConfig(bytes.NewReader(data)); err == nil {
info.Width = config.Width
info.Height = config.Height
if info.MimeType == "image/gif" {
// Just show the gif itself instead of a preview image for animated gifs
if gifConfig, err := gif.DecodeAll(bytes.NewReader(data)); err != nil {
// Still return the rest of the info even though it doesn't appear to be an actual gif
info.HasPreviewImage = true
err = NewLocAppError("GetInfoForBytes", "model.file_info.get.gif.app_error", nil, "name="+name)
} else {
info.HasPreviewImage = len(gifConfig.Image) == 1
}
} else {
info.HasPreviewImage = true
}
}
}
return info, err
}
func GetEtagForFileInfos(infos []*FileInfo) string {
if len(infos) == 0 {
return Etag()
}
var maxUpdateAt int64
for _, info := range infos {
if info.UpdateAt > maxUpdateAt {
maxUpdateAt = info.UpdateAt
}
}
return Etag(infos[0].PostId, maxUpdateAt)
}

View File

@ -12,7 +12,6 @@ type InitialLoad struct {
User *User `json:"user"`
TeamMembers []*TeamMember `json:"team_members"`
Teams []*Team `json:"teams"`
DirectProfiles map[string]*User `json:"direct_profiles"`
Preferences Preferences `json:"preferences"`
ClientCfg map[string]string `json:"client_cfg"`
LicenseCfg map[string]string `json:"license_cfg"`

View File

@ -43,6 +43,7 @@ type Features struct {
MHPNS *bool `json:"mhpns"`
SAML *bool `json:"saml"`
PasswordRequirements *bool `json:"password_requirements"`
// after we enabled more features for webrtc we'll need to control them with this
FutureFeatures *bool `json:"future_features"`
}

View File

@ -35,7 +35,8 @@ type Post struct {
Type string `json:"type"`
Props StringInterface `json:"props"`
Hashtags string `json:"hashtags"`
Filenames StringArray `json:"filenames"`
Filenames StringArray `json:"filenames,omitempty"` // Deprecated, do not use this field any more
FileIds StringArray `json:"file_ids,omitempty"`
PendingPostId string `json:"pending_post_id" db:"-"`
}
@ -118,6 +119,10 @@ func (o *Post) IsValid() *AppError {
return NewLocAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id)
}
if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > 150 {
return NewLocAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id)
}
if utf8.RuneCountInString(StringInterfaceToJson(o.Props)) > 8000 {
return NewLocAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id)
}
@ -145,15 +150,16 @@ func (o *Post) PreSave() {
if o.Filenames == nil {
o.Filenames = []string{}
}
if o.FileIds == nil {
o.FileIds = []string{}
}
}
func (o *Post) MakeNonNil() {
if o.Props == nil {
o.Props = make(map[string]interface{})
}
if o.Filenames == nil {
o.Filenames = []string{}
}
}
func (o *Post) AddProp(key string, value interface{}) {

View File

@ -11,7 +11,7 @@ import (
const (
SESSION_COOKIE_TOKEN = "MMAUTHTOKEN"
SESSION_CACHE_SIZE = 10000
SESSION_CACHE_SIZE = 25000
SESSION_PROP_PLATFORM = "platform"
SESSION_PROP_OS = "os"
SESSION_PROP_BROWSER = "browser"
@ -115,6 +115,10 @@ func (me *Session) IsMobileApp() bool {
(strings.HasPrefix(me.DeviceId, PUSH_NOTIFY_APPLE+":") || strings.HasPrefix(me.DeviceId, PUSH_NOTIFY_ANDROID+":"))
}
func (me *Session) GetUserRoles() []string {
return strings.Fields(me.Roles)
}
func SessionsToJson(o []*Session) string {
if b, err := json.Marshal(o); err != nil {
return "[]"

View File

@ -12,8 +12,9 @@ const (
STATUS_OFFLINE = "offline"
STATUS_AWAY = "away"
STATUS_ONLINE = "online"
STATUS_CACHE_SIZE = 10000
STATUS_CACHE_SIZE = 25000
STATUS_CHANNEL_TIMEOUT = 20000 // 20 seconds
STATUS_MIN_UPDATE_TIME = 120000 // 2 minutes
)
type Status struct {

View File

@ -100,7 +100,7 @@ func (o *Team) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
func (o *Team) IsValid(restrictTeamNames bool) *AppError {
func (o *Team) IsValid() *AppError {
if len(o.Id) != 26 {
return NewLocAppError("Team.IsValid", "model.team.is_valid.id.app_error", nil, "")
@ -130,7 +130,7 @@ func (o *Team) IsValid(restrictTeamNames bool) *AppError {
return NewLocAppError("Team.IsValid", "model.team.is_valid.url.app_error", nil, "id="+o.Id)
}
if restrictTeamNames && IsReservedTeamName(o.Name) {
if IsReservedTeamName(o.Name) {
return NewLocAppError("Team.IsValid", "model.team.is_valid.reserved.app_error", nil, "id="+o.Id)
}
@ -188,7 +188,7 @@ func IsValidTeamName(s string) bool {
return false
}
if len(s) <= 3 {
if len(s) <= 1 {
return false
}

View File

@ -9,10 +9,6 @@ import (
"strings"
)
const (
ROLE_TEAM_ADMIN = "admin"
)
type TeamMember struct {
TeamId string `json:"team_id"`
UserId string `json:"user_id"`
@ -59,48 +55,6 @@ func TeamMembersFromJson(data io.Reader) []*TeamMember {
}
}
func IsValidTeamRoles(teamRoles string) bool {
roles := strings.Split(teamRoles, " ")
for _, r := range roles {
if !isValidTeamRole(r) {
return false
}
}
return true
}
func isValidTeamRole(role string) bool {
if role == "" {
return true
}
if role == ROLE_TEAM_ADMIN {
return true
}
return false
}
func IsInTeamRole(teamRoles string, inRole string) bool {
roles := strings.Split(teamRoles, " ")
for _, r := range roles {
if r == inRole {
return true
}
}
return false
}
func (o *TeamMember) IsTeamAdmin() bool {
return IsInTeamRole(o.Roles, ROLE_TEAM_ADMIN)
}
func (o *TeamMember) IsValid() *AppError {
if len(o.TeamId) != 26 {
@ -111,11 +65,12 @@ func (o *TeamMember) IsValid() *AppError {
return NewLocAppError("TeamMember.IsValid", "model.team_member.is_valid.user_id.app_error", nil, "")
}
for _, role := range strings.Split(o.Roles, " ") {
if !(role == "" || role == ROLE_TEAM_ADMIN) {
return NewLocAppError("TeamMember.IsValid", "model.team_member.is_valid.role.app_error", nil, "role="+role)
}
}
return nil
}
func (o *TeamMember) PreUpdate() {
}
func (o *TeamMember) GetRoles() []string {
return strings.Fields(o.Roles)
}

View File

@ -0,0 +1,35 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type TeamStats struct {
TeamId string `json:"team_id"`
TotalMemberCount int64 `json:"total_member_count"`
ActiveMemberCount int64 `json:"active_member_count"`
}
func (o *TeamStats) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func TeamStatsFromJson(data io.Reader) *TeamStats {
decoder := json.NewDecoder(data)
var o TeamStats
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -15,7 +15,6 @@ import (
)
const (
ROLE_SYSTEM_ADMIN = "system_admin"
USER_NOTIFY_ALL = "all"
USER_NOTIFY_MENTION = "mention"
USER_NOTIFY_NONE = "none"
@ -233,14 +232,15 @@ func (u *User) Sanitize(options map[string]bool) {
if len(options) != 0 && !options["passwordupdate"] {
u.LastPasswordUpdate = 0
}
if len(options) != 0 && !options["authservice"] {
u.AuthService = ""
}
}
func (u *User) ClearNonProfileFields() {
u.Password = ""
u.AuthData = new(string)
*u.AuthData = ""
u.AuthService = ""
u.MfaActive = false
u.MfaSecret = ""
u.EmailVerified = false
u.AllowMarketing = false
@ -319,9 +319,17 @@ func (u *User) GetDisplayNameForPreference(nameFormat string) string {
return displayName
}
func (u *User) GetRoles() []string {
return strings.Fields(u.Roles)
}
func (u *User) GetRawRoles() string {
return u.Roles
}
func IsValidUserRoles(userRoles string) bool {
roles := strings.Split(userRoles, " ")
roles := strings.Fields(userRoles)
for _, r := range roles {
if !isValidRole(r) {
@ -329,21 +337,19 @@ func IsValidUserRoles(userRoles string) bool {
}
}
return true
}
func isValidRole(role string) bool {
if role == "" {
return true
}
if role == ROLE_SYSTEM_ADMIN {
return true
}
// Exclude just the system_admin role explicitly to prevent mistakes
if len(roles) == 1 && roles[0] == "system_admin" {
return false
}
return true
}
func isValidRole(roleId string) bool {
_, ok := BuiltInRoles[roleId]
return ok
}
// 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 {
@ -411,6 +417,26 @@ func UserMapFromJson(data io.Reader) map[string]*User {
}
}
func UserListToJson(u []*User) string {
b, err := json.Marshal(u)
if err != nil {
return ""
} else {
return string(b)
}
}
func UserListFromJson(data io.Reader) []*User {
decoder := json.NewDecoder(data)
var users []*User
err := decoder.Decode(&users)
if err == nil {
return users
} else {
return nil
}
}
// HashPassword generates a hash using the bcrypt.GenerateFromPassword
func HashPassword(password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)

View File

@ -0,0 +1,39 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type UserSearch struct {
Term string `json:"term"`
TeamId string `json:"team_id"`
InChannelId string `json:"in_channel_id"`
NotInChannelId string `json:"not_in_channel_id"`
AllowInactive bool `json:"allow_inactive"`
}
// ToJson convert a User to a json string
func (u *UserSearch) ToJson() string {
b, err := json.Marshal(u)
if err != nil {
return ""
} else {
return string(b)
}
}
// UserSearchFromJson will decode the input and return a User
func UserSearchFromJson(data io.Reader) *UserSearch {
decoder := json.NewDecoder(data)
var us UserSearch
err := decoder.Decode(&us)
if err == nil {
return &us
} else {
return nil
}
}

View File

@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/mail"
"net/url"
"regexp"
@ -74,13 +75,21 @@ func (er *AppError) ToJson() string {
// AppErrorFromJson will decode the input and return an AppError
func AppErrorFromJson(data io.Reader) *AppError {
decoder := json.NewDecoder(data)
str := ""
bytes, rerr := ioutil.ReadAll(data)
if rerr != nil {
str = rerr.Error()
} else {
str = string(bytes)
}
decoder := json.NewDecoder(strings.NewReader(str))
var er AppError
err := decoder.Decode(&er)
if err == nil {
return &er
} else {
return NewLocAppError("AppErrorFromJson", "model.utils.decode_json.app_error", nil, err.Error())
return NewLocAppError("AppErrorFromJson", "model.utils.decode_json.app_error", nil, "body: "+str)
}
}
@ -166,6 +175,23 @@ func ArrayFromJson(data io.Reader) []string {
}
}
func ArrayFromInterface(data interface{}) []string {
stringArray := []string{}
dataArray, ok := data.([]interface{})
if !ok {
return stringArray
}
for _, v := range dataArray {
if str, ok := v.(string); ok {
stringArray = append(stringArray, str)
}
}
return stringArray
}
func StringInterfaceToJson(objmap map[string]interface{}) string {
if b, err := json.Marshal(objmap); err != nil {
return ""
@ -227,58 +253,15 @@ func IsValidEmail(email string) bool {
}
var reservedName = []string{
"www",
"web",
"signup",
"login",
"admin",
"support",
"notify",
"test",
"demo",
"mail",
"team",
"channel",
"internal",
"localhost",
"dockerhost",
"stag",
"post",
"cluster",
"api",
"oauth",
}
var wwwStart = regexp.MustCompile(`^www`)
var betaStart = regexp.MustCompile(`^beta`)
var ciStart = regexp.MustCompile(`^ci`)
func GetSubDomain(s string) (string, string) {
s = strings.Replace(s, "http://", "", 1)
s = strings.Replace(s, "https://", "", 1)
match := wwwStart.MatchString(s)
if match {
return "", ""
}
match = betaStart.MatchString(s)
if match {
return "", ""
}
match = ciStart.MatchString(s)
if match {
return "", ""
}
parts := strings.Split(s, ".")
if len(parts) != 3 {
return "", ""
}
return parts[0], parts[1]
}
func IsValidChannelIdentifier(s string) bool {
if !IsValidAlphaNum(s, true) {
@ -413,6 +396,18 @@ func IsValidHttpsUrl(rawUrl string) bool {
return true
}
func IsValidTurnOrStunServer(rawUri string) bool {
if strings.Index(rawUri, "turn:") != 0 && strings.Index(rawUri, "stun:") != 0 {
return false
}
if _, err := url.ParseRequestURI(rawUri); err != nil {
return false
}
return true
}
func IsSafeLink(link *string) bool {
if link != nil {
if IsValidHttpUrl(*link) {
@ -426,3 +421,15 @@ func IsSafeLink(link *string) bool {
return true
}
func IsValidWebsocketUrl(rawUrl string) bool {
if strings.Index(rawUrl, "ws://") != 0 && strings.Index(rawUrl, "wss://") != 0 {
return false
}
if _, err := url.ParseRequestURI(rawUrl); err != nil {
return false
}
return true
}

View File

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

21
vendor/github.com/mattermost/platform/model/webrtc.go generated vendored Normal file
View File

@ -0,0 +1,21 @@
package model
import (
"encoding/json"
"io"
)
type GatewayResponse struct {
Status string `json:"janus"`
}
func GatewayResponseFromJson(data io.Reader) *GatewayResponse {
decoder := json.NewDecoder(data)
var o GatewayResponse
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -6,7 +6,6 @@ package model
import (
"encoding/json"
"github.com/gorilla/websocket"
"net/http"
)
type WebSocketClient struct {
@ -17,19 +16,18 @@ type WebSocketClient struct {
Sequence int64 // The ever-incrementing sequence attached to each WebSocket action
EventChannel chan *WebSocketEvent
ResponseChannel chan *WebSocketResponse
ListenError *AppError
}
// 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)
conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX+"/users/websocket", nil)
if err != nil {
return nil, NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
}
return &WebSocketClient{
client := &WebSocketClient{
url,
url + API_URL_SUFFIX,
conn,
@ -37,19 +35,26 @@ func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) {
1,
make(chan *WebSocketEvent, 100),
make(chan *WebSocketResponse, 100),
}, nil
nil,
}
client.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": authToken})
return client, 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)
wsc.Conn, _, err = websocket.DefaultDialer.Dial(wsc.ApiUrl+"/users/websocket", nil)
if err != nil {
return NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
}
wsc.EventChannel = make(chan *WebSocketEvent, 100)
wsc.ResponseChannel = make(chan *WebSocketResponse, 100)
wsc.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": wsc.AuthToken})
return nil
}
@ -59,10 +64,20 @@ func (wsc *WebSocketClient) Close() {
func (wsc *WebSocketClient) Listen() {
go func() {
defer func() {
wsc.Conn.Close()
close(wsc.EventChannel)
close(wsc.ResponseChannel)
}()
for {
var rawMsg json.RawMessage
var err error
if _, rawMsg, err = wsc.Conn.ReadMessage(); err != nil {
if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
wsc.ListenError = NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
}
return
}
@ -77,6 +92,7 @@ func (wsc *WebSocketClient) Listen() {
wsc.ResponseChannel <- &response
continue
}
}
}()
}
@ -107,3 +123,12 @@ func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
func (wsc *WebSocketClient) GetStatuses() {
wsc.SendMessage("get_statuses", nil)
}
// GetStatusesByIds will fetch certain user statuses based on ids and return
// a map of string statuses using user id as the key
func (wsc *WebSocketClient) GetStatusesByIds(userIds []string) {
data := map[string]interface{}{
"user_ids": userIds,
}
wsc.SendMessage("get_statuses_by_ids", data)
}

View File

@ -25,33 +25,57 @@ const (
WEBSOCKET_EVENT_EPHEMERAL_MESSAGE = "ephemeral_message"
WEBSOCKET_EVENT_STATUS_CHANGE = "status_change"
WEBSOCKET_EVENT_HELLO = "hello"
WEBSOCKET_EVENT_WEBRTC = "webrtc"
WEBSOCKET_AUTHENTICATION_CHALLENGE = "authentication_challenge"
)
type WebSocketMessage interface {
ToJson() string
IsValid() bool
DoPreComputeJson()
GetPreComputeJson() []byte
}
type WebsocketBroadcast struct {
OmitUsers map[string]bool `json:"omit_users"` // broadcast is omitted for users listed here
UserId string `json:"user_id"` // broadcast only occurs for this user
ChannelId string `json:"channel_id"` // broadcast only occurs for users in this channel
TeamId string `json:"team_id"` // broadcast only occurs for users in this team
}
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"`
Broadcast *WebsocketBroadcast `json:"broadcast"`
PreComputeJson []byte `json:"-"`
}
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 NewWebSocketEvent(event, teamId, channelId, userId string, omitUsers map[string]bool) *WebSocketEvent {
return &WebSocketEvent{Event: event, Data: make(map[string]interface{}),
Broadcast: &WebsocketBroadcast{TeamId: teamId, ChannelId: channelId, UserId: userId, OmitUsers: omitUsers}}
}
func (o *WebSocketEvent) IsValid() bool {
return o.Event != ""
}
func (o *WebSocketEvent) DoPreComputeJson() {
b, err := json.Marshal(o)
if err != nil {
o.PreComputeJson = []byte("")
} else {
o.PreComputeJson = b
}
}
func (o *WebSocketEvent) GetPreComputeJson() []byte {
return o.PreComputeJson
}
func (o *WebSocketEvent) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
@ -77,6 +101,7 @@ type WebSocketResponse struct {
SeqReply int64 `json:"seq_reply,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
Error *AppError `json:"error,omitempty"`
PreComputeJson []byte `json:"-"`
}
func (m *WebSocketResponse) Add(key string, value interface{}) {
@ -104,6 +129,19 @@ func (o *WebSocketResponse) ToJson() string {
}
}
func (o *WebSocketResponse) DoPreComputeJson() {
b, err := json.Marshal(o)
if err != nil {
o.PreComputeJson = []byte("")
} else {
o.PreComputeJson = b
}
}
func (o *WebSocketResponse) GetPreComputeJson() []byte {
return o.PreComputeJson
}
func WebSocketResponseFromJson(data io.Reader) *WebSocketResponse {
decoder := json.NewDecoder(data)
var o WebSocketResponse

View File

@ -1,5 +1,7 @@
package slack
import "encoding/json"
// AttachmentField contains information for an attachment field
// An Attachment can contain multiple of these
type AttachmentField struct {
@ -17,7 +19,7 @@ type AttachmentAction struct {
Style string `json:"style,omitempty"` // Optional. Allowed values: "default", "primary", "danger"
Type string `json:"type"` // Required. Must be set to "button"
Value string `json:"value,omitempty"` // Optional.
Confirm []ConfirmationField `json:"confirm,omitempty"` // Optional.
Confirm *ConfirmationField `json:"confirm,omitempty"` // Optional.
}
// AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction)
@ -72,5 +74,5 @@ type Attachment struct {
Footer string `json:"footer,omitempty"`
FooterIcon string `json:"footer_icon,omitempty"`
Ts int64 `json:"ts,omitempty"`
Ts json.Number `json:"ts,omitempty"`
}

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

@ -0,0 +1,44 @@
package slack
import (
"errors"
"net/url"
)
// Bot contains information about a bot
type Bot struct {
ID string `json:"id"`
Name string `json:"name"`
Deleted bool `json:"deleted"`
Icons Icons `json:"icons"`
}
type botResponseFull struct {
Bot `json:"bot,omitempty"` // GetBotInfo
SlackResponse
}
func botRequest(path string, values url.Values, debug bool) (*botResponseFull, error) {
response := &botResponseFull{}
err := post(path, values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// GetBotInfo will retrive the complete bot information
func (api *Client) GetBotInfo(bot string) (*Bot, error) {
values := url.Values{
"token": {api.config.token},
"bot": {bot},
}
response, err := botRequest("bots.info", values, api.debug)
if err != nil {
return nil, err
}
return &response.Bot, nil
}

View File

@ -18,7 +18,6 @@ type groupConversation struct {
Creator string `json:"creator"`
IsArchived bool `json:"is_archived"`
Members []string `json:"members"`
NumMembers int `json:"num_members,omitempty"`
Topic Topic `json:"topic"`
Purpose Purpose `json:"purpose"`
}

View File

@ -136,15 +136,9 @@ type Team struct {
// Icons XXX: needs further investigation
type Icons struct {
Image48 string `json:"image_48"`
}
// Bot contains information about a bot
type Bot struct {
ID string `json:"id"`
Name string `json:"name"`
Deleted bool `json:"deleted"`
Icons Icons `json:"icons"`
Image36 string `json:"image_36,omitempty"`
Image48 string `json:"image_48,omitempty"`
Image72 string `json:"image_72,omitempty"`
}
// Info contains various details about Users, Channels, Bots and the authenticated user.

View File

@ -9,6 +9,7 @@ import (
"io/ioutil"
"mime/multipart"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
@ -96,6 +97,13 @@ func postWithMultipartResponse(path string, filepath string, values url.Values,
return err
}
defer resp.Body.Close()
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != 200 {
logResponse(resp, debug)
return fmt.Errorf("Slack server error: %s.", resp.Status)
}
return parseResponseBody(resp.Body, &intf, debug)
}
@ -117,3 +125,16 @@ func parseAdminResponse(method string, teamName string, values url.Values, intf
endpoint := fmt.Sprintf(SLACK_WEB_API_FORMAT, teamName, method, time.Now().Unix())
return postForm(endpoint, values, intf, debug)
}
func logResponse(resp *http.Response, debug bool) error {
if debug {
text, err := httputil.DumpResponse(resp, true)
if err != nil {
return err
}
logger.Print(text)
}
return nil
}

View File

@ -3,6 +3,12 @@ package slack
import (
"errors"
"net/url"
"strconv"
)
const (
DEFAULT_LOGINS_COUNT = 100
DEFAULT_LOGINS_PAGE = 1
)
type TeamResponse struct {
@ -18,6 +24,41 @@ type TeamInfo struct {
Icon map[string]interface{} `json:"icon"`
}
type LoginResponse struct {
Logins []Login `json:"logins"`
Paging `json:"paging"`
SlackResponse
}
type Login struct {
UserID string `json:"user_id"`
Username string `json:"username"`
DateFirst int `json:"date_first"`
DateLast int `json:"date_last"`
Count int `json:"count"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
ISP string `json:"isp"`
Country string `json:"country"`
Region string `json:"region"`
}
// AccessLogParameters contains all the parameters necessary (including the optional ones) for a GetAccessLogs() request
type AccessLogParameters struct {
Count int
Page int
}
// NewAccessLogParameters provides an instance of AccessLogParameters with all the sane default values set
func NewAccessLogParameters() AccessLogParameters {
return AccessLogParameters{
Count: DEFAULT_LOGINS_COUNT,
Page: DEFAULT_LOGINS_PAGE,
}
}
func teamRequest(path string, values url.Values, debug bool) (*TeamResponse, error) {
response := &TeamResponse{}
err := post(path, values, response, debug)
@ -32,6 +73,19 @@ func teamRequest(path string, values url.Values, debug bool) (*TeamResponse, err
return response, nil
}
func accessLogsRequest(path string, values url.Values, debug bool) (*LoginResponse, error) {
response := &LoginResponse{}
err := post(path, values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
// GetTeamInfo gets the Team Information of the user
func (api *Client) GetTeamInfo() (*TeamInfo, error) {
values := url.Values{
@ -44,3 +98,22 @@ func (api *Client) GetTeamInfo() (*TeamInfo, error) {
}
return &response.Team, nil
}
// GetAccessLogs retrieves a page of logins according to the parameters given
func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging, error) {
values := url.Values{
"token": {api.config.token},
}
if params.Count != DEFAULT_LOGINS_COUNT {
values.Add("count", strconv.Itoa(params.Count))
}
if params.Page != DEFAULT_LOGINS_PAGE {
values.Add("page", strconv.Itoa(params.Page))
}
response, err := accessLogsRequest("team.accessLogs", values, api.debug)
if err != nil {
return nil, nil, err
}
return response.Logins, &response.Paging, nil
}

View File

@ -21,6 +21,8 @@ type UserProfile struct {
Image192 string `json:"image_192"`
ImageOriginal string `json:"image_original"`
Title string `json:"title"`
BotID string `json:"bot_id,omitempty"`
ApiAppID string `json:"api_app_id,omitempty"`
}
// User contains all the information of a user
@ -55,6 +57,39 @@ type UserPresence struct {
LastActivity JSONTime `json:"last_activity,omitempty"`
}
type UserIdentityResponse struct {
User UserIdentity `json:"user"`
Team TeamIdentity `json:"team"`
SlackResponse
}
type UserIdentity struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Image24 string `json:"image_24"`
Image32 string `json:"image_32"`
Image48 string `json:"image_48"`
Image72 string `json:"image_72"`
Image192 string `json:"image_192"`
Image512 string `json:"image_512"`
}
type TeamIdentity struct {
ID string `json:"id"`
Name string `json:"name"`
Domain string `json:"domain"`
Image34 string `json:"image_34"`
Image44 string `json:"image_44"`
Image68 string `json:"image_68"`
Image88 string `json:"image_88"`
Image102 string `json:"image_102"`
Image132 string `json:"image_132"`
Image230 string `json:"image_230"`
ImageDefault bool `json:"image_default"`
ImageOriginal string `json:"image_original"`
}
type userResponseFull struct {
Members []User `json:"members,omitempty"` // ListUsers
User `json:"user,omitempty"` // GetUserInfo
@ -138,3 +173,19 @@ func (api *Client) SetUserPresence(presence string) error {
return nil
}
// GetUserIdentity will retrieve user info available per identity scopes
func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) {
values := url.Values{
"token": {api.config.token},
}
response := &UserIdentityResponse{}
err := post("users.identity", values, response, api.debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}

View File

@ -90,6 +90,7 @@ func (rtm *RTM) connect(connectionCount int) (*Info, *websocket.Conn, error) {
rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}}
return nil, nil, sErr
}
// any other errors are treated as recoverable and we try again after
// sending the event along the IncomingEvents channel
rtm.IncomingEvents <- RTMEvent{"connection_error", &ConnectionErrorEvent{
@ -191,6 +192,18 @@ func (rtm *RTM) handleIncomingEvents(keepRunning <-chan bool) {
}
}
func (rtm *RTM) sendWithDeadline(msg interface{}) error {
// set a write deadline on the connection
if err := rtm.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil {
return err
}
if err := websocket.JSON.Send(rtm.conn, msg); err != nil {
return err
}
// remove write deadline
return rtm.conn.SetWriteDeadline(time.Time{})
}
// sendOutgoingMessage sends the given OutgoingMessage to the slack websocket.
//
// It does not currently detect if a outgoing message fails due to a disconnect
@ -204,8 +217,8 @@ func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) {
}}
return
}
err := websocket.JSON.Send(rtm.conn, msg)
if err != nil {
if err := rtm.sendWithDeadline(msg); err != nil {
rtm.IncomingEvents <- RTMEvent{"outgoing_error", &OutgoingErrorEvent{
Message: msg,
ErrorObj: err,
@ -227,8 +240,8 @@ func (rtm *RTM) ping() error {
rtm.pings[id] = time.Now()
msg := &Ping{ID: id, Type: "ping"}
err := websocket.JSON.Send(rtm.conn, msg)
if err != nil {
if err := rtm.sendWithDeadline(msg); err != nil {
rtm.Debugf("RTM Error sending 'PING %d': %s", id, err.Error())
return err
}

View File

@ -427,7 +427,7 @@ func (irc *Connection) Connect(server string) error {
}
irc.stopped = false
irc.Log.Printf("Connected to %s (%s)\n", irc.Server, irc.socket.RemoteAddr())
//irc.Log.Printf("Connected to %s (%s)\n", irc.Server, irc.socket.RemoteAddr())
irc.pwrite = make(chan string, 10)
irc.Error = make(chan error, 2)

View File

@ -1,5 +1,4 @@
Copyright (c) 2012 Péter Surányi. Portions Copyright (c) 2009 The Go
Authors. All rights reserved.
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

149
vendor/golang.org/x/crypto/nacl/secretbox/secretbox.go generated vendored Normal file
View File

@ -0,0 +1,149 @@
// Copyright 2012 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.
/*
Package secretbox encrypts and authenticates small messages.
Secretbox uses XSalsa20 and Poly1305 to encrypt and authenticate messages with
secret-key cryptography. The length of messages is not hidden.
It is the caller's responsibility to ensure the uniqueness of nonces—for
example, by using nonce 1 for the first message, nonce 2 for the second
message, etc. Nonces are long enough that randomly generated nonces have
negligible risk of collision.
This package is interoperable with NaCl: https://nacl.cr.yp.to/secretbox.html.
*/
package secretbox // import "golang.org/x/crypto/nacl/secretbox"
import (
"golang.org/x/crypto/poly1305"
"golang.org/x/crypto/salsa20/salsa"
)
// Overhead is the number of bytes of overhead when boxing a message.
const Overhead = poly1305.TagSize
// setup produces a sub-key and Salsa20 counter given a nonce and key.
func setup(subKey *[32]byte, counter *[16]byte, nonce *[24]byte, key *[32]byte) {
// We use XSalsa20 for encryption so first we need to generate a
// key and nonce with HSalsa20.
var hNonce [16]byte
copy(hNonce[:], nonce[:])
salsa.HSalsa20(subKey, &hNonce, key, &salsa.Sigma)
// The final 8 bytes of the original nonce form the new nonce.
copy(counter[:], nonce[16:])
}
// sliceForAppend takes a slice and a requested number of bytes. It returns a
// slice with the contents of the given slice followed by that many bytes and a
// second slice that aliases into it and contains only the extra bytes. If the
// original slice has sufficient capacity then no allocation is performed.
func sliceForAppend(in []byte, n int) (head, tail []byte) {
if total := len(in) + n; cap(in) >= total {
head = in[:total]
} else {
head = make([]byte, total)
copy(head, in)
}
tail = head[len(in):]
return
}
// Seal appends an encrypted and authenticated copy of message to out, which
// must not overlap message. The key and nonce pair must be unique for each
// distinct message and the output will be Overhead bytes longer than message.
func Seal(out, message []byte, nonce *[24]byte, key *[32]byte) []byte {
var subKey [32]byte
var counter [16]byte
setup(&subKey, &counter, nonce, key)
// The Poly1305 key is generated by encrypting 32 bytes of zeros. Since
// Salsa20 works with 64-byte blocks, we also generate 32 bytes of
// keystream as a side effect.
var firstBlock [64]byte
salsa.XORKeyStream(firstBlock[:], firstBlock[:], &counter, &subKey)
var poly1305Key [32]byte
copy(poly1305Key[:], firstBlock[:])
ret, out := sliceForAppend(out, len(message)+poly1305.TagSize)
// We XOR up to 32 bytes of message with the keystream generated from
// the first block.
firstMessageBlock := message
if len(firstMessageBlock) > 32 {
firstMessageBlock = firstMessageBlock[:32]
}
tagOut := out
out = out[poly1305.TagSize:]
for i, x := range firstMessageBlock {
out[i] = firstBlock[32+i] ^ x
}
message = message[len(firstMessageBlock):]
ciphertext := out
out = out[len(firstMessageBlock):]
// Now encrypt the rest.
counter[8] = 1
salsa.XORKeyStream(out, message, &counter, &subKey)
var tag [poly1305.TagSize]byte
poly1305.Sum(&tag, ciphertext, &poly1305Key)
copy(tagOut, tag[:])
return ret
}
// Open authenticates and decrypts a box produced by Seal and appends the
// message to out, which must not overlap box. The output will be Overhead
// bytes smaller than box.
func Open(out []byte, box []byte, nonce *[24]byte, key *[32]byte) ([]byte, bool) {
if len(box) < Overhead {
return nil, false
}
var subKey [32]byte
var counter [16]byte
setup(&subKey, &counter, nonce, key)
// The Poly1305 key is generated by encrypting 32 bytes of zeros. Since
// Salsa20 works with 64-byte blocks, we also generate 32 bytes of
// keystream as a side effect.
var firstBlock [64]byte
salsa.XORKeyStream(firstBlock[:], firstBlock[:], &counter, &subKey)
var poly1305Key [32]byte
copy(poly1305Key[:], firstBlock[:])
var tag [poly1305.TagSize]byte
copy(tag[:], box)
if !poly1305.Verify(&tag, box[poly1305.TagSize:], &poly1305Key) {
return nil, false
}
ret, out := sliceForAppend(out, len(box)-Overhead)
// We XOR up to 32 bytes of box with the keystream generated from
// the first block.
box = box[Overhead:]
firstMessageBlock := box
if len(firstMessageBlock) > 32 {
firstMessageBlock = firstMessageBlock[:32]
}
for i, x := range firstMessageBlock {
out[i] = firstBlock[32+i] ^ x
}
box = box[len(firstMessageBlock):]
out = out[len(firstMessageBlock):]
// Now decrypt the rest.
counter[8] = 1
salsa.XORKeyStream(out, box, &counter, &subKey)
return ret, true
}

27
vendor/golang.org/x/crypto/poly1305/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.

45
vendor/golang.org/x/crypto/poly1305/const_amd64.s generated vendored Normal file
View File

@ -0,0 +1,45 @@
// Copyright 2012 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.
// This code was translated into a form compatible with 6a from the public
// domain sources in SUPERCOP: http://bench.cr.yp.to/supercop.html
// +build amd64,!gccgo,!appengine
DATA ·SCALE(SB)/8, $0x37F4000000000000
GLOBL ·SCALE(SB), 8, $8
DATA ·TWO32(SB)/8, $0x41F0000000000000
GLOBL ·TWO32(SB), 8, $8
DATA ·TWO64(SB)/8, $0x43F0000000000000
GLOBL ·TWO64(SB), 8, $8
DATA ·TWO96(SB)/8, $0x45F0000000000000
GLOBL ·TWO96(SB), 8, $8
DATA ·ALPHA32(SB)/8, $0x45E8000000000000
GLOBL ·ALPHA32(SB), 8, $8
DATA ·ALPHA64(SB)/8, $0x47E8000000000000
GLOBL ·ALPHA64(SB), 8, $8
DATA ·ALPHA96(SB)/8, $0x49E8000000000000
GLOBL ·ALPHA96(SB), 8, $8
DATA ·ALPHA130(SB)/8, $0x4C08000000000000
GLOBL ·ALPHA130(SB), 8, $8
DATA ·DOFFSET0(SB)/8, $0x4330000000000000
GLOBL ·DOFFSET0(SB), 8, $8
DATA ·DOFFSET1(SB)/8, $0x4530000000000000
GLOBL ·DOFFSET1(SB), 8, $8
DATA ·DOFFSET2(SB)/8, $0x4730000000000000
GLOBL ·DOFFSET2(SB), 8, $8
DATA ·DOFFSET3(SB)/8, $0x4930000000000000
GLOBL ·DOFFSET3(SB), 8, $8
DATA ·DOFFSET3MINUSTWO128(SB)/8, $0x492FFFFE00000000
GLOBL ·DOFFSET3MINUSTWO128(SB), 8, $8
DATA ·HOFFSET0(SB)/8, $0x43300001FFFFFFFB
GLOBL ·HOFFSET0(SB), 8, $8
DATA ·HOFFSET1(SB)/8, $0x45300001FFFFFFFE
GLOBL ·HOFFSET1(SB), 8, $8
DATA ·HOFFSET2(SB)/8, $0x47300001FFFFFFFE
GLOBL ·HOFFSET2(SB), 8, $8
DATA ·HOFFSET3(SB)/8, $0x49300003FFFFFFFE
GLOBL ·HOFFSET3(SB), 8, $8
DATA ·ROUNDING(SB)/2, $0x137f
GLOBL ·ROUNDING(SB), 8, $2

32
vendor/golang.org/x/crypto/poly1305/poly1305.go generated vendored Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2012 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.
/*
Package poly1305 implements Poly1305 one-time message authentication code as specified in http://cr.yp.to/mac/poly1305-20050329.pdf.
Poly1305 is a fast, one-time authentication function. It is infeasible for an
attacker to generate an authenticator for a message without the key. However, a
key must only be used for a single message. Authenticating two different
messages with the same key allows an attacker to forge authenticators for other
messages with the same key.
Poly1305 was originally coupled with AES in order to make Poly1305-AES. AES was
used with a fixed key in order to generate one-time keys from an nonce.
However, in this package AES isn't used and the one-time key is specified
directly.
*/
package poly1305 // import "golang.org/x/crypto/poly1305"
import "crypto/subtle"
// TagSize is the size, in bytes, of a poly1305 authenticator.
const TagSize = 16
// Verify returns true if mac is a valid authenticator for m with the given
// key.
func Verify(mac *[16]byte, m []byte, key *[32]byte) bool {
var tmp [16]byte
Sum(&tmp, m, key)
return subtle.ConstantTimeCompare(tmp[:], mac[:]) == 1
}

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