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

Compare commits

...

149 Commits
v0.3 ... v0.8.0

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
Wim
6e410b096e Release v0.6.1 2016-09-17 15:34:59 +02:00
Wim
f9e5994348 Fix mattermost API change for UpdateLastViewedAt 2016-09-17 15:33:02 +02:00
Wim
ee77272cfd Release v0.6.0 2016-09-17 15:25:34 +02:00
Wim
16ed2aca6a Sync with mattermost 3.4.0 2016-09-17 15:19:18 +02:00
Wim
0f530e7902 Fix spinning for loop 2016-09-05 23:08:17 +02:00
Wim
4ed66ce20e Update documentation 2016-09-05 16:42:46 +02:00
Wim
b30e85836e Add Slack support 2016-09-05 16:34:37 +02:00
Wim
e449a97bd0 Release v0.6.0-beta2 2016-09-04 20:42:24 +02:00
Wim
39043f3fa4 Update documentation 2016-09-04 20:41:03 +02:00
Wim
12389d602e Add Gitter support 2016-09-04 20:04:43 +02:00
Wim
44144587a0 Get correct teamname for non-joined channels. Closes 42wim/matterircd#65 2016-09-01 00:15:48 +02:00
Wim
d0a30e354b Fix documentation layout 2016-08-20 18:16:22 +02:00
Wim
c261dc89d5 Fix documentation layout 2016-08-20 18:15:06 +02:00
Wim
c2c135bca2 Release v0.6.0-beta1 2016-08-20 18:09:00 +02:00
Wim
eb20cb237d Update documentation 2016-08-20 18:08:59 +02:00
Wim
106404d32f Fix info message 2016-08-20 18:08:59 +02:00
Wim
e06efbad9f Remove unused code 2016-08-20 18:08:58 +02:00
Wim
3311c7f923 Refactor handleReceive 2016-08-20 18:08:58 +02:00
Wim
3a6c655dfb Remove redundant function 2016-08-20 18:08:58 +02:00
Wim
e11d786775 Move nickformatting into bridge 2016-08-20 18:08:57 +02:00
Wim
889b6debc4 Add Connect() to Bridger interface 2016-08-20 18:08:57 +02:00
Wim
9cb3413d9c Add Enable per section (protocol) instead of in general section 2016-08-20 18:08:57 +02:00
Wim
131826e1d1 Fix crash on exit 2016-08-19 22:58:42 +02:00
Wim
96e21dd051 Add documentation about breaking API changes for mattermost 3.3.0. Start work on 0.6.0-dev 2016-08-15 21:11:50 +02:00
Wim
32e5f396e7 Make sure login works after logout 2016-08-15 20:27:36 +02:00
Wim
6c6000dbbd Update code to mattermost 3.3.0 API changes 2016-08-15 18:49:17 +02:00
Wim
24defcb970 Sync with mattermost 3.3.0 2016-08-15 18:47:31 +02:00
Wim
a1a11a88b3 Fix nil pointers 2016-08-14 23:04:28 +02:00
Wim
a997ae29ad Add StatusLoop(), keeps connection alive 2016-08-14 22:54:57 +02:00
Wim
ff94796700 Refactor bridge. Allows bridging between every protocol 2016-08-14 22:44:59 +02:00
Wim
1f72ca4c4e Add initial XMPP support 2016-08-14 22:40:26 +02:00
Wim
46faad8b57 Vendor go-xmpp 2016-08-14 22:40:25 +02:00
Wim
30f30364d5 Release v0.5.0 2016-07-27 22:42:59 +02:00
Wim
073d90da88 Fix docker build 2016-07-23 00:03:54 +02:00
Wim
c769e23a9a Fix crash on invalid team 2016-07-22 23:47:25 +02:00
Wim
9db48f4794 Update readme 2016-07-22 23:36:54 +02:00
Wim
911c597377 Sync with mattermost 3.2.0 2016-07-22 23:15:59 +02:00
Wim
28244ffd9a Fix pointer reuse problem 2016-07-22 23:04:08 +02:00
Wim
3e38c7945c Actually add sasl.go 2016-07-22 22:51:11 +02:00
Wim
79ffb76f6e Add (PLAIN) SASL support 2016-07-21 23:47:44 +02:00
Wim
5fe4b749cf Do not check bindaddress when not using the server 2016-07-17 22:15:19 +02:00
Wim
6991d85da9 Add FAQ section 2016-07-15 21:59:14 +02:00
Wim
c1c187a1ab Fix markdown 2016-07-12 22:00:38 +02:00
Wim
055d12e3ef Release v0.5.0-beta1 2016-07-12 21:32:15 +02:00
Wim
b49429d722 Add migration info 2016-07-12 01:23:36 +02:00
Wim
815c7f8d64 Update version 2016-07-12 01:07:37 +02:00
Wim
c879f79456 Update documentation 2016-07-12 01:02:56 +02:00
Wim
3bc25f4707 Update documentation 2016-07-12 00:25:32 +02:00
Wim
300cfe044a Remove token check 2016-07-12 00:23:36 +02:00
Wim
fb586f4a96 Remove Port from IRC config. Specify it with server 2016-07-11 23:30:42 +02:00
Wim
ced371bece Add port to BindAddress 2016-07-11 23:22:56 +02:00
Wim
a87cac1982 Remove multiple Token config. Use same channel setup as from matterbridge-plus 2016-07-11 22:55:58 +02:00
Wim
8fb5c7afa6 Remove UseSlackCircumfix. Use RemoteNickFormat 2016-07-11 21:26:13 +02:00
Wim
aceb830378 Converge with matterbridge-plus 2016-07-11 21:23:33 +02:00
Wim
0f2976c5ce Release v0.4.2 2016-06-23 20:31:12 +02:00
Wim
78b17977c5 Fix mattermost 3.1.0 API change. Closes #24 2016-06-23 20:29:51 +02:00
Wim
6ec77e06ea Sync with mattermost 3.1.0 2016-06-23 20:28:05 +02:00
Wim
e48db67649 Fix sample documentation. Closes #23 2016-06-16 21:01:46 +02:00
Wim
e03f331f55 Add Dockerfile 2016-05-21 16:30:39 +02:00
Wim
ff5aeeb1e1 Add support for ignoring messages from specific users. 2016-05-21 16:03:19 +02:00
Wim
33844fa60c Commit mattermost vendoring 2016-05-21 14:14:08 +02:00
Wim
85faa43145 Release v0.4 2016-05-15 23:05:03 +02:00
Wim
59e6abcc11 Sync with mattermost 3.0 2016-05-15 23:02:30 +02:00
Wim
38e3bbe5c9 Mention go1.6 requiremÃent 2016-04-25 21:40:26 +02:00
Wim
51265d5464 Sync with mattermost 2.2.0 release 2016-04-16 20:39:43 +02:00
Wim
de4c780410 Vendor libs 2016-04-10 23:39:38 +02:00
Wim
6b18257185 Move common code to matterbridge-plus bridge package 2016-03-23 00:25:04 +01:00
Wim
4b1ebaf7d5 Update documentation and sample config 2016-03-22 00:42:49 +01:00
Wim
93db74e7e1 Add -debug option. Run go fmt. Some cleanups 2016-03-22 00:26:27 +01:00
0e6fe4070a Merge pull request #16 from fdevibe/nickserv-auth
Add Freenode style NickServ-based authentication.
2016-03-22 00:04:26 +01:00
69b534ee99 Add Freenode style NickServ-based authentication.
If the server sends a NOTICE requesting identification, identify.
2016-03-21 23:52:10 +01:00
71a504945b Merge pull request #15 from fdevibe/async-connect
Handle connects asynchroneously.
2016-03-21 23:49:52 +01:00
99ac7dc114 Handle connects asynchroneously.
On connect events, update the nick with what's actually used and
set up channels as configured.
2016-03-21 14:24:25 +01:00
4984473c1b Merge pull request #14 from fdevibe/enable-users-2
Enable !users command and add a table formatter
2016-03-18 23:48:05 +01:00
3fcce2d8a0 Limit number of columns to number of IRC nicks.
Don't add more columns to the IRC nicks table than the total number of
nicks in the IRC channel.
2016-03-18 18:13:17 -04:00
a53e699112 Remove unnecessary break in switch block. 2016-03-18 18:09:55 -04:00
f29822db02 Add double newline if the message is markup and prefixed.
If the message is prefixed with the sender nick, it will break markup
formatting on the same line. This commit introduces a very rudimentary
markup checker, and if the message is deemed to be markup in those
cases, the space between sender nick and message is replaced by a
double newline.
2016-03-18 18:03:15 -04:00
a63433e41b Move new config parameters from IRC to Mattermost.
PrefixMessagesWithNick, NickFormatter and NicksPerRow.
2016-03-18 15:54:14 -04:00
e0379ca5af Enable !users command and add a table formatter
* Listen to wildcarded events, currently handle RPL_NAMREPLY.
* Add a formatter to present nick lists from IRC as a table.
2016-03-18 15:19:13 -04:00
4759ee6132 Merge pull request #12 from fdevibe/msgprefix
Add config option to prefix messages (IRC->MM) with nick
2016-03-18 12:50:52 +01:00
5ec94fdb43 Add config option to prefix messages (IRC->MM) with nick
If username overriding isn't enabled on the Mattermost server, this is
required for Mattermost users to see who sent a message from IRC.
2016-03-18 06:09:29 -04:00
362 changed files with 63042 additions and 287 deletions

11
Dockerfile Normal file
View File

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

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.

121
README.md
View File

@ -1,17 +1,52 @@
# matterbridge
Simple bridge between mattermost, IRC, XMPP, Gitter, Slack and Discord
Simple bridge between mattermost and IRC. Uses the in/outgoing webhooks.
Relays public channel messages between mattermost and IRC.
* Relays public channel messages between multiple mattermost, IRC, XMPP, Gitter, Slack and Discord. Pick and mix.
* Supports multiple channels.
* 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).
Requires mattermost 1.2.0+
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.
There is also [matterbridge-plus] (https://github.com/42wim/matterbridge-plus) which uses the mattermost API and needs a dedicated user (bot). But requires no incoming/outgoing webhook setup.
## Changelog
Since v0.7.0 the configuration has changed. 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://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/tag/v0.3)
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/)
* 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.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.
#### API version
* A dedicated user(bot) on your mattermost instance.
## building
Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH] (https://golang.org/doc/code.html#GOPATH)
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
@ -31,63 +66,24 @@ matterbridge
3) Now you can run matterbridge.
```
Usage of matterbridge:
-conf="matterbridge.conf": config file
Usage of ./matterbridge:
-conf string
config file (default "matterbridge.toml")
-debug
enable debug
-version
show version
```
Matterbridge will:
* start a webserver listening on the port specified in the configuration.
* connect to specified irc server and channel.
* send messages from mattermost to irc and vice versa, messages in mattermost will appear with irc-nick
## config
### matterbridge
matterbridge looks for matterbridge.conf in current directory. (use -conf to specify another file)
matterbridge looks for matterbridge.toml in current directory. (use -conf to specify another file)
Look at matterbridge.conf.sample for an example
```
[IRC]
server="irc.freenode.net"
port=6667
UseTLS=false
SkipTLSVerify=true
nick="matterbot"
channel="#matterbridge"
UseSlackCircumfix=false
[mattermost]
#url is your incoming webhook url (account settings - integrations - incoming webhooks)
url="http://mattermost.yourdomain.com/hooks/incomingwebhookkey"
#port the bridge webserver will listen on
port=9999
#address the webserver will bind to
BindAddress="0.0.0.0"
showjoinpart=true #show irc users joining and parting
#the token you get from the outgoing webhook in mattermost. If empty no token check will be done.
#if you use multiple IRC channel (see below, this must be empty!)
token=yourtokenfrommattermost
#disable certificate checking (selfsigned certificates)
#SkipTLSVerify=true
#multiple channel config
#token you can find in your outgoing webhook
[Token "outgoingwebhooktoken1"]
IRCChannel="#off-topic"
MMChannel="off-topic"
[Token "outgoingwebhooktoken2"]
IRCChannel="#testing"
MMChannel="testing"
[general]
#request your API key on https://github.com/giphy/GiphyAPI. This is a public beta key
GiphyApiKey="dc6zaTOxFJmzC"
```
Look at [matterbridge.toml.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for an example.
### mattermost
You'll have to configure the incoming en outgoing webhooks.
#### webhooks version
You'll have to configure the incoming and outgoing webhooks.
* incoming webhooks
Go to "account settings" - integrations - "incoming webhooks".
@ -98,5 +94,16 @@ This URL should be set in the matterbridge.conf in the [mattermost] section (see
Go to "account settings" - integrations - "outgoing webhooks".
Choose a channel (the same as the one from incoming webhooks) and fill in the address and port of the server matterbridge will run on.
e.g. http://192.168.1.1:9999 (9999 is the port specified in [mattermost] section of matterbridge.conf)
e.g. http://192.168.1.1:9999 (192.168.1.1:9999 is the BindAddress specified in [mattermost] section of matterbridge.conf)
## FAQ
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.toml.
If you're running the plus version you'll need to:
* setting ```PrefixMessagesWithNick``` to ```true``` in ```mattermost``` section of your matterbridge.toml.
Also look at the ```RemoteNickFormat``` setting.

45
bridge/bridge.go Normal file
View File

@ -0,0 +1,45 @@
package bridge
import (
"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"
"strings"
)
type Bridge interface {
Send(msg config.Message) error
Name() string
Connect() error
FullOrigin() string
Origin() string
Protocol() string
JoinChannel(channel string) error
}
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":
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":
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
}

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

@ -0,0 +1,141 @@
package config
import (
"github.com/BurntSushi/toml"
"log"
"os"
"reflect"
"strings"
)
type Message struct {
Text string
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 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
if _, err := toml.DecodeFile(cfgfile, &cfg); err != nil {
log.Fatal(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 ""
}

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

@ -0,0 +1,135 @@
package bgitter
import (
"github.com/42wim/go-gitter"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"strings"
)
type Bgitter struct {
c *gitter.Gitter
Config *config.Protocol
Remote chan config.Message
protocol string
origin string
Users []gitter.User
Rooms []gitter.Room
}
var flog *log.Entry
var protocol = "gitter"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, origin string, c chan config.Message) *Bgitter {
b := &Bgitter{}
b.Config = &cfg
b.Remote = c
b.protocol = protocol
b.origin = origin
return b
}
func (b *Bgitter) Connect() error {
var err error
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.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 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, nick+msg.Text+" ")
}
func (b *Bgitter) getRoomID(channel string) string {
for _, v := range b.Rooms {
if v.URI == channel {
return v.ID
}
}
return ""
}
func (b *Bgitter) 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
}

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

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

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

@ -0,0 +1,262 @@
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 Birc struct {
i *irc.Connection
Nick string
names map[string][]string
Config *config.Protocol
origin string
protocol string
Remote chan config.Message
connected chan struct{}
Local chan config.Message // local queue for flood control
}
var flog *log.Entry
var protocol = "irc"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, origin string, c chan config.Message) *Birc {
b := &Birc{}
b.Config = &cfg
b.Nick = b.Config.Nick
b.Remote = c
b.names = make(map[string][]string)
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.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.Server)
if err != nil {
return err
}
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 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 {
flog.Debugf("Receiving %#v", msg)
if msg.FullOrigin == b.FullOrigin() {
return nil
}
if strings.HasPrefix(msg.Text, "!") {
b.Command(&msg)
return nil
}
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.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.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.Debug("Registering callbacks")
i := b.i
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_NAMREPLY, b.storeNames)
i.AddCallback(ircm.NOTICE, b.handleNotice)
//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.Debugf("PING/PONG")
})
i.AddCallback("*", b.handleOther)
// we are now fully connected
b.connected <- struct{}{}
}
func (b *Birc) handleNotice(event *irc.Event) {
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) {
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) {
// 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()
// 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.Errorf("Invalid time stamp: %s", event.Arguments[3])
}
user := parts[0]
if len(parts) > 1 {
user += " [" + parts[1] + "]"
}
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) storeNames(event *irc.Event) {
channel := event.Arguments[2]
b.names[channel] = append(
b.names[channel],
strings.Split(strings.TrimSpace(event.Message()), " ")...)
}
func (b *Birc) formatnicks(nicks []string, continued bool) string {
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

@ -0,0 +1,182 @@
package bmattermost
import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus"
)
type MMhook struct {
mh *matterhook.Client
}
type MMapi struct {
mc *matterclient.MMClient
mmMap map[string]string
mmIgnoreNicks []string
}
type MMMessage struct {
Text string
Channel string
Username string
}
type Bmattermost struct {
MMhook
MMapi
Config *config.Protocol
Remote chan config.Message
name string
origin string
protocol string
TeamId string
}
var flog *log.Entry
var protocol = "mattermost"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, origin string, c chan config.Message) *Bmattermost {
b := &Bmattermost{}
b.Config = &cfg
b.origin = origin
b.Remote = c
b.protocol = "mattermost"
b.name = cfg.Name
b.mmMap = make(map[string]string)
return b
}
func (b *Bmattermost) Command(cmd string) string {
return ""
}
func (b *Bmattermost) Connect() error {
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.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.Info("Connection succeeded")
b.TeamId = b.mc.GetTeamId()
go b.mc.WsReceiver()
}
go b.handleMatter()
return nil
}
func (b *Bmattermost) FullOrigin() string {
return b.protocol + "." + b.origin
}
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) 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 {
*/
message = nick + " " + message
//}
}
if !b.Config.UseAPI {
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.Channel = channel
matterMessage.UserName = nick
matterMessage.Type = ""
matterMessage.Text = message
err := b.mh.Send(matterMessage)
if err != nil {
flog.Info(err)
return err
}
return nil
}
b.mc.PostMessage(b.mc.GetChannelId(channel, ""), message)
return nil
}
func (b *Bmattermost) handleMatter() {
flog.Debugf("Choosing API based Mattermost connection: %t", b.Config.UseAPI)
mchan := make(chan *MMMessage)
if b.Config.UseAPI {
go b.handleMatterClient(mchan)
} else {
go b.handleMatterHook(mchan)
}
for message := range mchan {
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
// 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
}
}
}
func (b *Bmattermost) handleMatterHook(mchan chan *MMMessage) {
for {
message := b.mh.Receive()
flog.Debugf("Receiving from matterhook %#v", message)
m := &MMMessage{}
m.Username = message.UserName
m.Text = message.Text
m.Channel = message.ChannelName
mchan <- m
}
}

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

@ -0,0 +1,238 @@
package bslack
import (
"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus"
"github.com/nlopes/slack"
"strings"
"time"
)
type MMMessage struct {
Text string
Channel string
Username string
Raw *slack.MessageEvent
}
type Bslack struct {
mh *matterhook.Client
sc *slack.Client
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": protocol})
}
func New(cfg config.Protocol, origin string, c chan config.Message) *Bslack {
b := &Bslack{}
b.Config = &cfg
b.Remote = c
b.protocol = protocol
b.origin = origin
return b
}
func (b *Bslack) Command(cmd string) string {
return ""
}
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.Token)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
}
flog.Info("Connection succeeded")
go b.handleSlack()
return nil
}
func (b *Bslack) FullOrigin() string {
return b.protocol + "." + b.origin
}
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) 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.Config.UseAPI {
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.Channel = channel
matterMessage.UserName = nick
matterMessage.Type = ""
matterMessage.Text = message
err := b.mh.Send(matterMessage)
if err != nil {
flog.Info(err)
return err
}
return nil
}
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) 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, fmt.Errorf("%s: channel %s not found (no channels found)", b.FullOrigin(), name)
}
for _, channel := range b.channels {
if channel.Name == name {
return &channel, nil
}
}
return nil, fmt.Errorf("%s: channel %s not found", b.FullOrigin(), name)
}
func (b *Bslack) handleSlack() {
flog.Debugf("Choosing API based slack connection: %t", b.Config.UseAPI)
mchan := make(chan *MMMessage)
if b.Config.UseAPI {
go b.handleSlackClient(mchan)
} else {
go b.handleMatterHook(mchan)
}
time.Sleep(time.Second)
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.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) {
count := 0
for msg := range b.rtm.IncomingEvents {
switch ev := msg.Data.(type) {
case *slack.MessageEvent:
// 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
}
user, err := b.rtm.GetUserInfo(ev.User)
if err != nil {
continue
}
m := &MMMessage{}
m.Username = user.Name
m.Channel = channel.Name
m.Text = ev.Text
m.Raw = ev
mchan <- m
}
count++
case *slack.OutgoingErrorEvent:
flog.Debugf("%#v", ev.Error())
case *slack.ConnectedEvent:
b.channels = ev.Info.Channels
b.si = ev.Info
b.Users, _ = b.sc.GetUsers()
case *slack.InvalidAuthEvent:
flog.Fatalf("Invalid Token %#v", ev)
default:
}
}
}
func (b *Bslack) handleMatterHook(mchan chan *MMMessage) {
for {
message := b.mh.Receive()
flog.Debugf("receiving from matterhook (slack) %#v", message)
m := &MMMessage{}
m.Username = message.UserName
m.Text = message.Text
m.Channel = message.ChannelName
mchan <- m
}
}

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

@ -0,0 +1,139 @@
package bxmpp
import (
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/mattn/go-xmpp"
"strings"
"time"
)
type Bxmpp struct {
xc *xmpp.Client
xmppMap map[string]string
Config *config.Protocol
origin string
protocol string
Remote chan config.Message
}
var flog *log.Entry
var protocol = "xmpp"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, origin string, c chan config.Message) *Bxmpp {
b := &Bxmpp{}
b.xmppMap = make(map[string]string)
b.Config = &cfg
b.protocol = protocol
b.origin = origin
b.Remote = c
return b
}
func (b *Bxmpp) Connect() error {
var err error
flog.Infof("Connecting %s", b.Config.Server)
b.xc, err = b.createXMPP()
if err != nil {
flog.Debugf("%#v", err)
return err
}
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 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 {
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.Server,
User: b.Config.Jid,
Password: b.Config.Password,
NoTLS: true,
StartTLS: true,
//StartTLS: false,
Debug: true,
Session: true,
Status: "",
StatusMessage: "",
Resource: "",
InsecureAllowUnencryptedAuth: false,
//InsecureAllowUnencryptedAuth: true,
}
var err error
b.xc, err = options.NewClient()
return b.xc, err
}
func (b *Bxmpp) xmppKeepAlive() {
go func() {
ticker := time.NewTicker(90 * time.Second)
for {
select {
case <-ticker.C:
b.xc.Send(xmpp.Chat{})
}
}
}()
}
func (b *Bxmpp) handleXmpp() error {
for {
m, err := b.xc.Recv()
if err != nil {
return err
}
switch v := m.(type) {
case xmpp.Chat:
var channel, nick string
if v.Type == "groupchat" {
s := strings.Split(v.Remote, "@")
if len(s) == 2 {
channel = s[0]
}
s = strings.Split(s[1], "/")
if len(s) == 2 {
nick = s[1]
}
if nick != b.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:
// do nothing
}
}
}

121
changelog.md Normal file
View File

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

View File

@ -1,50 +0,0 @@
package main
import (
"gopkg.in/gcfg.v1"
"io/ioutil"
"log"
)
type Config struct {
IRC struct {
UseTLS bool
SkipTLSVerify bool
Server string
Port int
Nick string
Password string
Channel string
UseSlackCircumfix bool
}
Mattermost struct {
URL string
Port int
ShowJoinPart bool
Token string
IconURL string
SkipTLSVerify bool
BindAddress string
Channel string
}
Token map[string]*struct {
IRCChannel string
MMChannel string
}
General struct {
GiphyAPIKey string
}
}
func NewConfig(cfgfile string) *Config {
var cfg Config
content, err := ioutil.ReadFile(cfgfile)
if err != nil {
log.Fatal(err)
}
err = gcfg.ReadStringInto(&cfg, string(content))
if err != nil {
log.Fatal("Failed to parse "+cfgfile+":", err)
}
return &cfg
}

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

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

View File

@ -1,163 +1,60 @@
package main
import (
"crypto/tls"
"flag"
"github.com/42wim/matterbridge/matterhook"
"github.com/peterhellberg/giphy"
"github.com/thoj/go-ircevent"
"log"
"strconv"
"strings"
"time"
"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway"
"github.com/42wim/matterbridge/gateway/samechannel"
log "github.com/Sirupsen/logrus"
)
type Bridge struct {
i *irc.Connection
m *matterhook.Client
cmap map[string]string
*Config
}
var version = "0.8.0"
func NewBridge(name string, config *Config) *Bridge {
b := &Bridge{}
b.Config = config
b.cmap = make(map[string]string)
if len(b.Config.Token) > 0 {
for _, val := range b.Config.Token {
b.cmap[val.IRCChannel] = val.MMChannel
}
}
b.m = matterhook.New(b.Config.Mattermost.URL,
matterhook.Config{Port: b.Config.Mattermost.Port, Token: b.Config.Mattermost.Token,
InsecureSkipVerify: b.Config.Mattermost.SkipTLSVerify,
BindAddress: b.Config.Mattermost.BindAddress})
b.i = b.createIRC(name)
go b.handleMatter()
return b
}
func (b *Bridge) createIRC(name string) *irc.Connection {
i := irc.IRC(b.Config.IRC.Nick, b.Config.IRC.Nick)
i.UseTLS = b.Config.IRC.UseTLS
i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.IRC.SkipTLSVerify}
if b.Config.IRC.Password != "" {
i.Password = b.Config.IRC.Password
}
i.Connect(b.Config.IRC.Server + ":" + strconv.Itoa(b.Config.IRC.Port))
time.Sleep(time.Second)
log.Println("Joining", b.Config.IRC.Channel, "as", b.Config.IRC.Nick)
i.Join(b.Config.IRC.Channel)
for _, val := range b.Config.Token {
log.Println("Joining", val.IRCChannel, "as", b.Config.IRC.Nick)
i.Join(val.IRCChannel)
}
i.AddCallback("PRIVMSG", b.handlePrivMsg)
i.AddCallback("CTCP_ACTION", b.handlePrivMsg)
if b.Config.Mattermost.ShowJoinPart {
i.AddCallback("JOIN", b.handleJoinPart)
i.AddCallback("PART", b.handleJoinPart)
}
//i.AddCallback("353", b.handleOther)
return i
}
func (b *Bridge) handlePrivMsg(event *irc.Event) {
msg := ""
if event.Code == "CTCP_ACTION" {
msg = event.Nick + " "
}
msg += event.Message()
b.Send("irc-"+event.Nick, msg, b.getMMChannel(event.Arguments[0]))
}
func (b *Bridge) handleJoinPart(event *irc.Event) {
b.Send(b.Config.IRC.Nick, "irc-"+event.Nick+" "+strings.ToLower(event.Code)+"s "+event.Message(), b.getMMChannel(event.Arguments[0]))
//b.SendType(b.Config.IRC.Nick, "irc-"+event.Nick+" "+strings.ToLower(event.Code)+"s "+event.Message(), b.getMMChannel(event.Arguments[0]), "join_leave")
}
func (b *Bridge) handleOther(event *irc.Event) {
switch event.Code {
case "353":
log.Println("handleOther", b.getMMChannel(event.Arguments[0]))
b.Send(b.Config.IRC.Nick, event.Message()+" currently on IRC", b.getMMChannel(event.Arguments[0]))
}
}
func (b *Bridge) Send(nick string, message string, channel string) error {
return b.SendType(nick, message, channel, "")
}
func (b *Bridge) SendType(nick string, message string, channel string, mtype string) error {
matterMessage := matterhook.OMessage{IconURL: b.Config.Mattermost.IconURL}
matterMessage.Channel = channel
matterMessage.UserName = nick
matterMessage.Text = message
matterMessage.Type = mtype
err := b.m.Send(matterMessage)
if err != nil {
log.Println(err)
return err
}
return nil
}
func (b *Bridge) handleMatter() {
var username string
for {
message := b.m.Receive()
username = message.UserName + ": "
if b.Config.IRC.UseSlackCircumfix {
username = "<" + message.UserName + "> "
}
cmd := strings.Fields(message.Text)[0]
switch cmd {
case "!users":
log.Println("received !users from", message.UserName)
b.i.SendRaw("NAMES " + b.getIRCChannel(message.Token))
case "!gif":
message.Text = b.giphyRandom(strings.Fields(strings.Replace(message.Text, "!gif ", "", 1)))
b.Send(b.Config.IRC.Nick, message.Text, b.getIRCChannel(message.Token))
}
texts := strings.Split(message.Text, "\n")
for _, text := range texts {
b.i.Privmsg(b.getIRCChannel(message.Token), username+text)
}
}
}
func (b *Bridge) giphyRandom(query []string) string {
g := giphy.DefaultClient
if b.Config.General.GiphyAPIKey != "" {
g.APIKey = b.Config.General.GiphyAPIKey
}
res, err := g.Random(query)
if err != nil {
return "error"
}
return res.Data.FixedHeightDownsampledURL
}
func (b *Bridge) getMMChannel(ircChannel string) string {
mmchannel, ok := b.cmap[ircChannel]
if !ok {
mmchannel = b.Config.Mattermost.Channel
}
return mmchannel
}
func (b *Bridge) getIRCChannel(token string) string {
ircchannel := b.Config.IRC.Channel
_, ok := b.Config.Token[token]
if ok {
ircchannel = b.Config.Token[token].IRCChannel
}
return ircchannel
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")
flag.Parse()
NewBridge("matterbot", NewConfig(*flagConfig))
if *flagVersion {
fmt.Println("version:", version)
return
}
flag.Parse()
if *flagDebug {
log.Info("enabling debug")
log.SetLevel(log.DebugLevel)
}
fmt.Println("running version", version)
cfg := config.NewConfig(*flagConfig)
for _, gw := range cfg.SameChannelGateway {
if !gw.Enable {
continue
}
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 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

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

View File

@ -10,8 +10,8 @@ import (
"io"
"io/ioutil"
"log"
"net"
"net/http"
"strconv"
)
// OMessage for mattermost incoming webhook. (send to mattermost)
@ -27,15 +27,19 @@ type OMessage struct {
// IMessage for mattermost outgoing webhook. (received from mattermost)
type IMessage struct {
BotID string `schema:"bot_id"`
BotName string `schema:"bot_name"`
Token string `schema:"token"`
TeamID string `schema:"team_id"`
TeamDomain string `schema:"team_domain"`
ChannelID string `schema:"channel_id"`
ServiceID string `schema:"service_id"`
ChannelName string `schema:"channel_name"`
Timestamp string `schema:"timestamp"`
UserID string `schema:"user_id"`
UserName string `schema:"user_name"`
PostId string `schema:"post_id"`
RawText string `schema:"raw_text"`
ServiceId string `schema:"service_id"`
Text string `schema:"text"`
TriggerWord string `schema:"trigger_word"`
}
@ -51,7 +55,6 @@ type Client struct {
// Config for client.
type Config struct {
Port int // Port to listen on.
BindAddress string // Address to listen on
Token string // Only allow this token from Mattermost. (Allow everything when empty)
InsecureSkipVerify bool // disable certificate checking
@ -61,15 +64,15 @@ type Config struct {
// New Mattermost client.
func New(url string, config Config) *Client {
c := &Client{Url: url, In: make(chan IMessage), Out: make(chan OMessage), Config: config}
if c.Port == 0 {
c.Port = 9999
}
c.BindAddress += ":"
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
}
c.httpclient = &http.Client{Transport: tr}
if !c.DisableServer {
_, _, err := net.SplitHostPort(c.BindAddress)
if err != nil {
log.Fatalf("incorrect bindaddress %s", c.BindAddress)
}
go c.StartServer()
}
return c
@ -79,8 +82,8 @@ func New(url string, config Config) *Client {
func (c *Client) StartServer() {
mux := http.NewServeMux()
mux.Handle("/", c)
log.Printf("Listening on http://%v:%v...\n", c.BindAddress, c.Port)
if err := http.ListenAndServe((c.BindAddress + strconv.Itoa(c.Port)), mux); err != nil {
log.Printf("Listening on http://%v...\n", c.BindAddress)
if err := http.ListenAndServe(c.BindAddress, mux); err != nil {
log.Fatal(err)
}
}

50
migration.md Normal file
View File

@ -0,0 +1,50 @@
# Breaking changes from 0.4 to 0.5 for matterbridge (webhooks version)
## IRC section
### Server
Port removed, added to server
```
server="irc.freenode.net"
port=6667
```
changed to
```
server="irc.freenode.net:6667"
```
### Channel
Removed see Channels section below
### UseSlackCircumfix=true
Removed, can be done by using ```RemoteNickFormat="<{NICK}> "```
## Mattermost section
### BindAddress
Port removed, added to BindAddress
```
BindAddress="0.0.0.0"
port=9999
```
changed to
```
BindAddress="0.0.0.0:9999"
```
### Token
Removed
## Channels section
```
[Token "outgoingwebhooktoken1"]
IRCChannel="#off-topic"
MMChannel="off-topic"
```
changed to
```
[Channel "channelnameofchoice"]
IRC="#off-topic"
Mattermost="off-topic"
```

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

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

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

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

430
vendor/github.com/42wim/go-gitter/gitter.go generated vendored Normal file
View File

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

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

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

221
vendor/github.com/42wim/go-gitter/stream.go generated vendored Normal file
View File

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

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

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

View File

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

View File

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

View File

@ -0,0 +1,68 @@
package bridge
import (
"gopkg.in/gcfg.v1"
"io/ioutil"
"log"
)
type Config struct {
IRC struct {
UseTLS bool
SkipTLSVerify bool
Server string
Port int
Nick string
Password string
Channel string
UseSlackCircumfix bool
NickServNick string
NickServPassword string
RemoteNickFormat string
IgnoreNicks string
}
Mattermost struct {
URL string
Port int
ShowJoinPart bool
Token string
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
}
Token map[string]*struct {
IRCChannel string
MMChannel string
}
Channel map[string]*struct {
IRC string
Mattermost string
}
General struct {
GiphyAPIKey string
}
}
func NewConfig(cfgfile string) *Config {
var cfg Config
content, err := ioutil.ReadFile(cfgfile)
if err != nil {
log.Fatal(err)
}
err = gcfg.ReadStringInto(&cfg, string(content))
if err != nil {
log.Fatal("Failed to parse "+cfgfile+":", err)
}
return &cfg
}

View File

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

View File

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

View File

@ -0,0 +1,441 @@
package matterclient
import (
"crypto/tls"
"errors"
log "github.com/Sirupsen/logrus"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/jpillora/backoff"
"github.com/mattermost/platform/model"
)
type Credentials struct {
Login string
Team string
Pass string
Server string
NoTLS bool
SkipTLSVerify bool
}
type Message struct {
Raw *model.Message
Post *model.Post
Team string
Channel string
Username string
Text string
}
type MMClient struct {
*Credentials
Client *model.Client
WsClient *websocket.Conn
WsQuit bool
WsAway bool
Channels *model.ChannelList
MoreChannels *model.ChannelList
User *model.User
Users map[string]*model.User
MessageChan chan *Message
Team *model.Team
log *log.Entry
}
func New(login, pass, team, server string) *MMClient {
cred := &Credentials{Login: login, Pass: pass, Team: team, Server: server}
mmclient := &MMClient{Credentials: cred, MessageChan: make(chan *Message, 100)}
mmclient.log = log.WithFields(log.Fields{"module": "matterclient"})
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
return mmclient
}
func (m *MMClient) SetLogLevel(level string) {
l, err := log.ParseLevel(level)
if err != nil {
log.SetLevel(log.InfoLevel)
return
}
log.SetLevel(l)
}
func (m *MMClient) Login() error {
if m.WsQuit {
return nil
}
b := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
uriScheme := "https://"
wsScheme := "wss://"
if m.NoTLS {
uriScheme = "http://"
wsScheme = "ws://"
}
// login to mattermost
m.Client = model.NewClient(uriScheme + m.Credentials.Server)
m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
var myinfo *model.Result
var appErr *model.AppError
var logmsg = "trying login"
for {
m.log.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
m.log.Debugf(logmsg+" with ", model.SESSION_COOKIE_TOKEN)
token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=")
m.Client.HttpClient.Jar = m.createCookieJar(token[1])
m.Client.MockSession(token[1])
myinfo, appErr = m.Client.GetMe("")
if myinfo.Data.(*model.User) == nil {
m.log.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
return errors.New("invalid " + model.SESSION_COOKIE_TOKEN)
}
} else {
myinfo, appErr = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
}
if appErr != nil {
d := b.Duration()
m.log.Debug(appErr.DetailedError)
if !strings.Contains(appErr.DetailedError, "connection refused") &&
!strings.Contains(appErr.DetailedError, "invalid character") {
if appErr.Message == "" {
return errors.New(appErr.DetailedError)
}
return errors.New(appErr.Message)
}
m.log.Debugf("LOGIN: %s, reconnecting in %s", appErr, d)
time.Sleep(d)
logmsg = "retrying login"
continue
}
break
}
// reset timer
b.Reset()
initLoad, _ := m.Client.GetInitialLoad()
initData := initLoad.Data.(*model.InitialLoad)
m.User = initData.User
for _, v := range initData.Teams {
m.log.Debugf("trying %s (id: %s)", v.Name, v.Id)
if v.Name == m.Credentials.Team {
m.Client.SetTeamId(v.Id)
m.Team = v
m.log.Debugf("GetallTeamListings: found id %s for team %s", v.Id, v.Name)
break
}
}
if m.Team == nil {
return errors.New("team not found")
}
// setup websocket connection
wsurl := wsScheme + m.Credentials.Server + "/api/v3/users/websocket"
header := http.Header{}
header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
m.log.Debug("WsClient: making connection")
var err error
for {
wsDialer := &websocket.Dialer{Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
m.WsClient, _, err = wsDialer.Dial(wsurl, header)
if err != nil {
d := b.Duration()
m.log.Debugf("WSS: %s, reconnecting in %s", err, d)
time.Sleep(d)
continue
}
break
}
b.Reset()
// populating users
m.UpdateUsers()
// populating channels
m.UpdateChannels()
return nil
}
func (m *MMClient) WsReceiver() {
var rmsg model.Message
for {
if m.WsQuit {
m.log.Debug("exiting WsReceiver")
return
}
if err := m.WsClient.ReadJSON(&rmsg); err != nil {
m.log.Error("error:", err)
// reconnect
m.Login()
}
if rmsg.Action == "ping" {
m.handleWsPing()
continue
}
msg := &Message{Raw: &rmsg, Team: m.Credentials.Team}
m.parseMessage(msg)
m.MessageChan <- msg
}
}
func (m *MMClient) handleWsPing() {
m.log.Debug("Ws PING")
if !m.WsQuit && !m.WsAway {
m.log.Debug("Ws PONG")
m.WsClient.WriteMessage(websocket.PongMessage, []byte{})
}
}
func (m *MMClient) parseMessage(rmsg *Message) {
switch rmsg.Raw.Action {
case model.ACTION_POSTED:
m.parseActionPost(rmsg)
/*
case model.ACTION_USER_REMOVED:
m.handleWsActionUserRemoved(&rmsg)
case model.ACTION_USER_ADDED:
m.handleWsActionUserAdded(&rmsg)
*/
}
}
func (m *MMClient) parseActionPost(rmsg *Message) {
data := model.PostFromJson(strings.NewReader(rmsg.Raw.Props["post"]))
// log.Println("receiving userid", data.UserId)
// we don't have the user, refresh the userlist
if m.Users[data.UserId] == nil {
m.UpdateUsers()
}
rmsg.Username = m.Users[data.UserId].Username
rmsg.Channel = m.GetChannelName(data.ChannelId)
// direct message
if strings.Contains(rmsg.Channel, "__") {
//log.Println("direct message")
rcvusers := strings.Split(rmsg.Channel, "__")
if rcvusers[0] != m.User.Id {
rmsg.Channel = m.Users[rcvusers[0]].Username
} else {
rmsg.Channel = m.Users[rcvusers[1]].Username
}
}
rmsg.Text = data.Message
rmsg.Post = data
return
}
func (m *MMClient) UpdateUsers() error {
mmusers, _ := m.Client.GetProfilesForDirectMessageList(m.Team.Id)
m.Users = mmusers.Data.(map[string]*model.User)
return nil
}
func (m *MMClient) UpdateChannels() error {
mmchannels, _ := m.Client.GetChannels("")
m.Channels = mmchannels.Data.(*model.ChannelList)
mmchannels, _ = m.Client.GetMoreChannels("")
m.MoreChannels = mmchannels.Data.(*model.ChannelList)
return nil
}
func (m *MMClient) GetChannelName(id string) string {
for _, channel := range append(m.Channels.Channels, m.MoreChannels.Channels...) {
if channel.Id == id {
return channel.Name
}
}
// not found? could be a new direct message from mattermost. Try to update and check again
m.UpdateChannels()
for _, channel := range append(m.Channels.Channels, m.MoreChannels.Channels...) {
if channel.Id == id {
return channel.Name
}
}
return ""
}
func (m *MMClient) GetChannelId(name string) string {
for _, channel := range append(m.Channels.Channels, m.MoreChannels.Channels...) {
if channel.Name == name {
return channel.Id
}
}
return ""
}
func (m *MMClient) GetChannelHeader(id string) string {
for _, channel := range append(m.Channels.Channels, m.MoreChannels.Channels...) {
if channel.Id == id {
return channel.Header
}
}
return ""
}
func (m *MMClient) PostMessage(channel string, text string) {
post := &model.Post{ChannelId: m.GetChannelId(channel), Message: text}
m.Client.CreatePost(post)
}
func (m *MMClient) JoinChannel(channel string) error {
cleanChan := strings.Replace(channel, "#", "", 1)
if m.GetChannelId(cleanChan) == "" {
return errors.New("failed to join")
}
for _, c := range m.Channels.Channels {
if c.Name == cleanChan {
m.log.Debug("Not joining ", cleanChan, " already joined.")
return nil
}
}
m.log.Debug("Joining ", cleanChan)
_, err := m.Client.JoinChannel(m.GetChannelId(cleanChan))
if err != nil {
return errors.New("failed to join")
}
// m.SyncChannel(m.getMMChannelId(strings.Replace(channel, "#", "", 1)), strings.Replace(channel, "#", "", 1))
return nil
}
func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList {
res, err := m.Client.GetPostsSince(channelId, time)
if err != nil {
return nil
}
return res.Data.(*model.PostList)
}
func (m *MMClient) SearchPosts(query string) *model.PostList {
res, err := m.Client.SearchPosts(query, false)
if err != nil {
return nil
}
return res.Data.(*model.PostList)
}
func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList {
res, err := m.Client.GetPosts(channelId, 0, limit, "")
if err != nil {
return nil
}
return res.Data.(*model.PostList)
}
func (m *MMClient) GetPublicLink(filename string) string {
res, err := m.Client.GetPublicLink(filename)
if err != nil {
return ""
}
return res.Data.(string)
}
func (m *MMClient) GetPublicLinks(filenames []string) []string {
var output []string
for _, f := range filenames {
res, err := m.Client.GetPublicLink(f)
if err != nil {
continue
}
output = append(output, res.Data.(string))
}
return output
}
func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
data := make(map[string]string)
data["channel_id"] = channelId
data["channel_header"] = header
m.log.Debugf("updating channelheader %#v, %#v", channelId, header)
_, err := m.Client.UpdateChannelHeader(data)
if err != nil {
log.Error(err)
}
}
func (m *MMClient) UpdateLastViewed(channelId string) {
m.log.Debugf("posting lastview %#v", channelId)
_, err := m.Client.UpdateLastViewedAt(channelId)
if err != nil {
m.log.Error(err)
}
}
func (m *MMClient) UsernamesInChannel(channelName string) []string {
ceiRes, err := m.Client.GetChannelExtraInfo(m.GetChannelId(channelName), 5000, "")
if err != nil {
m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelName, err)
return []string{}
}
extra := ceiRes.Data.(*model.ChannelExtra)
result := []string{}
for _, member := range extra.Members {
result = append(result, member.Username)
}
return result
}
func (m *MMClient) createCookieJar(token string) *cookiejar.Jar {
var cookies []*http.Cookie
jar, _ := cookiejar.New(nil)
firstCookie := &http.Cookie{
Name: "MMAUTHTOKEN",
Value: token,
Path: "/",
Domain: m.Credentials.Server,
}
cookies = append(cookies, firstCookie)
cookieURL, _ := url.Parse("https://" + m.Credentials.Server)
jar.SetCookies(cookieURL, cookies)
return jar
}
func (m *MMClient) GetOtherUserDM(channel string) *model.User {
m.UpdateUsers()
var rcvuser *model.User
if strings.Contains(channel, "__") {
rcvusers := strings.Split(channel, "__")
if rcvusers[0] != m.User.Id {
rcvuser = m.Users[rcvusers[0]]
} else {
rcvuser = m.Users[rcvusers[1]]
}
}
return rcvuser
}
func (m *MMClient) SendDirectMessage(toUserId string, msg string) {
m.log.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
var channel string
// We don't have a DM with this user yet.
if m.GetChannelId(toUserId+"__"+m.User.Id) == "" && m.GetChannelId(m.User.Id+"__"+toUserId) == "" {
// create DM channel
_, err := m.Client.CreateDirectChannel(toUserId)
if err != nil {
m.log.Debugf("SendDirectMessage to %#v failed: %s", toUserId, err)
}
// update our channels
mmchannels, _ := m.Client.GetChannels("")
m.Channels = mmchannels.Data.(*model.ChannelList)
}
// build the channel name
if toUserId > m.User.Id {
channel = m.User.Id + "__" + toUserId
} else {
channel = toUserId + "__" + m.User.Id
}
// build & send the message
msg = strings.Replace(msg, "\r", "", -1)
post := &model.Post{ChannelId: m.GetChannelId(channel), Message: msg}
m.Client.CreatePost(post)
}

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
}

21
vendor/github.com/Sirupsen/logrus/LICENSE generated vendored Normal file
View File

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

26
vendor/github.com/Sirupsen/logrus/doc.go generated vendored Normal file
View File

@ -0,0 +1,26 @@
/*
Package logrus is a structured logger for Go, completely API compatible with the standard library logger.
The simplest way to use Logrus is simply the package-level exported logger:
package main
import (
log "github.com/Sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
"number": 1,
"size": 10,
}).Info("A walrus appears")
}
Output:
time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10
For a full guide visit https://github.com/Sirupsen/logrus
*/
package logrus

264
vendor/github.com/Sirupsen/logrus/entry.go generated vendored Normal file
View File

@ -0,0 +1,264 @@
package logrus
import (
"bytes"
"fmt"
"io"
"os"
"time"
)
// Defines the key when adding errors using WithError.
var ErrorKey = "error"
// An entry is the final or intermediate Logrus logging entry. It contains all
// the fields passed with WithField{,s}. It's finally logged when Debug, Info,
// Warn, Error, Fatal or Panic is called on it. These objects can be reused and
// passed around as much as you wish to avoid field duplication.
type Entry struct {
Logger *Logger
// Contains all the fields set by the user.
Data Fields
// Time at which the log entry was created
Time time.Time
// Level the log entry was logged at: Debug, Info, Warn, Error, Fatal or Panic
Level Level
// Message passed to Debug, Info, Warn, Error, Fatal or Panic
Message string
}
func NewEntry(logger *Logger) *Entry {
return &Entry{
Logger: logger,
// Default is three fields, give a little extra room
Data: make(Fields, 5),
}
}
// Returns a reader for the entry, which is a proxy to the formatter.
func (entry *Entry) Reader() (*bytes.Buffer, error) {
serialized, err := entry.Logger.Formatter.Format(entry)
return bytes.NewBuffer(serialized), err
}
// Returns the string representation from the reader and ultimately the
// formatter.
func (entry *Entry) String() (string, error) {
reader, err := entry.Reader()
if err != nil {
return "", err
}
return reader.String(), err
}
// Add an error as single field (using the key defined in ErrorKey) to the Entry.
func (entry *Entry) WithError(err error) *Entry {
return entry.WithField(ErrorKey, err)
}
// Add a single field to the Entry.
func (entry *Entry) WithField(key string, value interface{}) *Entry {
return entry.WithFields(Fields{key: value})
}
// Add a map of fields to the Entry.
func (entry *Entry) WithFields(fields Fields) *Entry {
data := make(Fields, len(entry.Data)+len(fields))
for k, v := range entry.Data {
data[k] = v
}
for k, v := range fields {
data[k] = v
}
return &Entry{Logger: entry.Logger, Data: data}
}
// This function is not declared with a pointer value because otherwise
// race conditions will occur when using multiple goroutines
func (entry Entry) log(level Level, msg string) {
entry.Time = time.Now()
entry.Level = level
entry.Message = msg
if err := entry.Logger.Hooks.Fire(level, &entry); err != nil {
entry.Logger.mu.Lock()
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
entry.Logger.mu.Unlock()
}
reader, err := entry.Reader()
if err != nil {
entry.Logger.mu.Lock()
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
entry.Logger.mu.Unlock()
}
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
_, err = io.Copy(entry.Logger.Out, reader)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
// To avoid Entry#log() returning a value that only would make sense for
// panic() to use in Entry#Panic(), we avoid the allocation by checking
// directly here.
if level <= PanicLevel {
panic(&entry)
}
}
func (entry *Entry) Debug(args ...interface{}) {
if entry.Logger.Level >= DebugLevel {
entry.log(DebugLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Print(args ...interface{}) {
entry.Info(args...)
}
func (entry *Entry) Info(args ...interface{}) {
if entry.Logger.Level >= InfoLevel {
entry.log(InfoLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Warn(args ...interface{}) {
if entry.Logger.Level >= WarnLevel {
entry.log(WarnLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Warning(args ...interface{}) {
entry.Warn(args...)
}
func (entry *Entry) Error(args ...interface{}) {
if entry.Logger.Level >= ErrorLevel {
entry.log(ErrorLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Fatal(args ...interface{}) {
if entry.Logger.Level >= FatalLevel {
entry.log(FatalLevel, fmt.Sprint(args...))
}
os.Exit(1)
}
func (entry *Entry) Panic(args ...interface{}) {
if entry.Logger.Level >= PanicLevel {
entry.log(PanicLevel, fmt.Sprint(args...))
}
panic(fmt.Sprint(args...))
}
// Entry Printf family functions
func (entry *Entry) Debugf(format string, args ...interface{}) {
if entry.Logger.Level >= DebugLevel {
entry.Debug(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Infof(format string, args ...interface{}) {
if entry.Logger.Level >= InfoLevel {
entry.Info(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Printf(format string, args ...interface{}) {
entry.Infof(format, args...)
}
func (entry *Entry) Warnf(format string, args ...interface{}) {
if entry.Logger.Level >= WarnLevel {
entry.Warn(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Warningf(format string, args ...interface{}) {
entry.Warnf(format, args...)
}
func (entry *Entry) Errorf(format string, args ...interface{}) {
if entry.Logger.Level >= ErrorLevel {
entry.Error(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Fatalf(format string, args ...interface{}) {
if entry.Logger.Level >= FatalLevel {
entry.Fatal(fmt.Sprintf(format, args...))
}
os.Exit(1)
}
func (entry *Entry) Panicf(format string, args ...interface{}) {
if entry.Logger.Level >= PanicLevel {
entry.Panic(fmt.Sprintf(format, args...))
}
}
// Entry Println family functions
func (entry *Entry) Debugln(args ...interface{}) {
if entry.Logger.Level >= DebugLevel {
entry.Debug(entry.sprintlnn(args...))
}
}
func (entry *Entry) Infoln(args ...interface{}) {
if entry.Logger.Level >= InfoLevel {
entry.Info(entry.sprintlnn(args...))
}
}
func (entry *Entry) Println(args ...interface{}) {
entry.Infoln(args...)
}
func (entry *Entry) Warnln(args ...interface{}) {
if entry.Logger.Level >= WarnLevel {
entry.Warn(entry.sprintlnn(args...))
}
}
func (entry *Entry) Warningln(args ...interface{}) {
entry.Warnln(args...)
}
func (entry *Entry) Errorln(args ...interface{}) {
if entry.Logger.Level >= ErrorLevel {
entry.Error(entry.sprintlnn(args...))
}
}
func (entry *Entry) Fatalln(args ...interface{}) {
if entry.Logger.Level >= FatalLevel {
entry.Fatal(entry.sprintlnn(args...))
}
os.Exit(1)
}
func (entry *Entry) Panicln(args ...interface{}) {
if entry.Logger.Level >= PanicLevel {
entry.Panic(entry.sprintlnn(args...))
}
}
// Sprintlnn => Sprint no newline. This is to get the behavior of how
// fmt.Sprintln where spaces are always added between operands, regardless of
// their type. Instead of vendoring the Sprintln implementation to spare a
// string allocation, we do the simplest thing.
func (entry *Entry) sprintlnn(args ...interface{}) string {
msg := fmt.Sprintln(args...)
return msg[:len(msg)-1]
}

View File

@ -0,0 +1,50 @@
package main
import (
"github.com/Sirupsen/logrus"
)
var log = logrus.New()
func init() {
log.Formatter = new(logrus.JSONFormatter)
log.Formatter = new(logrus.TextFormatter) // default
log.Level = logrus.DebugLevel
}
func main() {
defer func() {
err := recover()
if err != nil {
log.WithFields(logrus.Fields{
"omg": true,
"err": err,
"number": 100,
}).Fatal("The ice breaks!")
}
}()
log.WithFields(logrus.Fields{
"animal": "walrus",
"number": 8,
}).Debug("Started observing beach")
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(logrus.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(logrus.Fields{
"temperature": -4,
}).Debug("Temperature changes")
log.WithFields(logrus.Fields{
"animal": "orca",
"size": 9009,
}).Panic("It's over 9000!")
}

View File

@ -0,0 +1,30 @@
package main
import (
"github.com/Sirupsen/logrus"
"gopkg.in/gemnasium/logrus-airbrake-hook.v2"
)
var log = logrus.New()
func init() {
log.Formatter = new(logrus.TextFormatter) // default
log.Hooks.Add(airbrake.NewHook(123, "xyz", "development"))
}
func main() {
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(logrus.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(logrus.Fields{
"omg": true,
"number": 100,
}).Fatal("The ice breaks!")
}

193
vendor/github.com/Sirupsen/logrus/exported.go generated vendored Normal file
View File

@ -0,0 +1,193 @@
package logrus
import (
"io"
)
var (
// std is the name of the standard logger in stdlib `log`
std = New()
)
func StandardLogger() *Logger {
return std
}
// SetOutput sets the standard logger output.
func SetOutput(out io.Writer) {
std.mu.Lock()
defer std.mu.Unlock()
std.Out = out
}
// SetFormatter sets the standard logger formatter.
func SetFormatter(formatter Formatter) {
std.mu.Lock()
defer std.mu.Unlock()
std.Formatter = formatter
}
// SetLevel sets the standard logger level.
func SetLevel(level Level) {
std.mu.Lock()
defer std.mu.Unlock()
std.Level = level
}
// GetLevel returns the standard logger level.
func GetLevel() Level {
std.mu.Lock()
defer std.mu.Unlock()
return std.Level
}
// AddHook adds a hook to the standard logger hooks.
func AddHook(hook Hook) {
std.mu.Lock()
defer std.mu.Unlock()
std.Hooks.Add(hook)
}
// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
func WithError(err error) *Entry {
return std.WithField(ErrorKey, err)
}
// WithField creates an entry from the standard logger and adds a field to
// it. If you want multiple fields, use `WithFields`.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithField(key string, value interface{}) *Entry {
return std.WithField(key, value)
}
// WithFields creates an entry from the standard logger and adds multiple
// fields to it. This is simply a helper for `WithField`, invoking it
// once for each field.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithFields(fields Fields) *Entry {
return std.WithFields(fields)
}
// Debug logs a message at level Debug on the standard logger.
func Debug(args ...interface{}) {
std.Debug(args...)
}
// Print logs a message at level Info on the standard logger.
func Print(args ...interface{}) {
std.Print(args...)
}
// Info logs a message at level Info on the standard logger.
func Info(args ...interface{}) {
std.Info(args...)
}
// Warn logs a message at level Warn on the standard logger.
func Warn(args ...interface{}) {
std.Warn(args...)
}
// Warning logs a message at level Warn on the standard logger.
func Warning(args ...interface{}) {
std.Warning(args...)
}
// Error logs a message at level Error on the standard logger.
func Error(args ...interface{}) {
std.Error(args...)
}
// Panic logs a message at level Panic on the standard logger.
func Panic(args ...interface{}) {
std.Panic(args...)
}
// Fatal logs a message at level Fatal on the standard logger.
func Fatal(args ...interface{}) {
std.Fatal(args...)
}
// Debugf logs a message at level Debug on the standard logger.
func Debugf(format string, args ...interface{}) {
std.Debugf(format, args...)
}
// Printf logs a message at level Info on the standard logger.
func Printf(format string, args ...interface{}) {
std.Printf(format, args...)
}
// Infof logs a message at level Info on the standard logger.
func Infof(format string, args ...interface{}) {
std.Infof(format, args...)
}
// Warnf logs a message at level Warn on the standard logger.
func Warnf(format string, args ...interface{}) {
std.Warnf(format, args...)
}
// Warningf logs a message at level Warn on the standard logger.
func Warningf(format string, args ...interface{}) {
std.Warningf(format, args...)
}
// Errorf logs a message at level Error on the standard logger.
func Errorf(format string, args ...interface{}) {
std.Errorf(format, args...)
}
// Panicf logs a message at level Panic on the standard logger.
func Panicf(format string, args ...interface{}) {
std.Panicf(format, args...)
}
// Fatalf logs a message at level Fatal on the standard logger.
func Fatalf(format string, args ...interface{}) {
std.Fatalf(format, args...)
}
// Debugln logs a message at level Debug on the standard logger.
func Debugln(args ...interface{}) {
std.Debugln(args...)
}
// Println logs a message at level Info on the standard logger.
func Println(args ...interface{}) {
std.Println(args...)
}
// Infoln logs a message at level Info on the standard logger.
func Infoln(args ...interface{}) {
std.Infoln(args...)
}
// Warnln logs a message at level Warn on the standard logger.
func Warnln(args ...interface{}) {
std.Warnln(args...)
}
// Warningln logs a message at level Warn on the standard logger.
func Warningln(args ...interface{}) {
std.Warningln(args...)
}
// Errorln logs a message at level Error on the standard logger.
func Errorln(args ...interface{}) {
std.Errorln(args...)
}
// Panicln logs a message at level Panic on the standard logger.
func Panicln(args ...interface{}) {
std.Panicln(args...)
}
// Fatalln logs a message at level Fatal on the standard logger.
func Fatalln(args ...interface{}) {
std.Fatalln(args...)
}

48
vendor/github.com/Sirupsen/logrus/formatter.go generated vendored Normal file
View File

@ -0,0 +1,48 @@
package logrus
import "time"
const DefaultTimestampFormat = time.RFC3339
// The Formatter interface is used to implement a custom Formatter. It takes an
// `Entry`. It exposes all the fields, including the default ones:
//
// * `entry.Data["msg"]`. The message passed from Info, Warn, Error ..
// * `entry.Data["time"]`. The timestamp.
// * `entry.Data["level"]. The level the entry was logged at.
//
// Any additional fields added with `WithField` or `WithFields` are also in
// `entry.Data`. Format is expected to return an array of bytes which are then
// logged to `logger.Out`.
type Formatter interface {
Format(*Entry) ([]byte, error)
}
// This is to not silently overwrite `time`, `msg` and `level` fields when
// dumping it. If this code wasn't there doing:
//
// logrus.WithField("level", 1).Info("hello")
//
// Would just silently drop the user provided level. Instead with this code
// it'll logged as:
//
// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
//
// It's not exported because it's still using Data in an opinionated way. It's to
// avoid code duplication between the two default formatters.
func prefixFieldClashes(data Fields) {
_, ok := data["time"]
if ok {
data["fields.time"] = data["time"]
}
_, ok = data["msg"]
if ok {
data["fields.msg"] = data["msg"]
}
_, ok = data["level"]
if ok {
data["fields.level"] = data["level"]
}
}

View File

@ -0,0 +1,61 @@
package logstash
import (
"encoding/json"
"fmt"
"github.com/Sirupsen/logrus"
)
// Formatter generates json in logstash format.
// Logstash site: http://logstash.net/
type LogstashFormatter struct {
Type string // if not empty use for logstash type field.
// TimestampFormat sets the format used for timestamps.
TimestampFormat string
}
func (f *LogstashFormatter) Format(entry *logrus.Entry) ([]byte, error) {
fields := make(logrus.Fields)
for k, v := range entry.Data {
fields[k] = v
}
fields["@version"] = 1
if f.TimestampFormat == "" {
f.TimestampFormat = logrus.DefaultTimestampFormat
}
fields["@timestamp"] = entry.Time.Format(f.TimestampFormat)
// set message field
v, ok := entry.Data["message"]
if ok {
fields["fields.message"] = v
}
fields["message"] = entry.Message
// set level field
v, ok = entry.Data["level"]
if ok {
fields["fields.level"] = v
}
fields["level"] = entry.Level.String()
// set type field
if f.Type != "" {
v, ok = entry.Data["type"]
if ok {
fields["fields.type"] = v
}
fields["type"] = f.Type
}
serialized, err := json.Marshal(fields)
if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
}
return append(serialized, '\n'), nil
}

34
vendor/github.com/Sirupsen/logrus/hooks.go generated vendored Normal file
View File

@ -0,0 +1,34 @@
package logrus
// A hook to be fired when logging on the logging levels returned from
// `Levels()` on your implementation of the interface. Note that this is not
// fired in a goroutine or a channel with workers, you should handle such
// functionality yourself if your call is non-blocking and you don't wish for
// the logging calls for levels returned from `Levels()` to block.
type Hook interface {
Levels() []Level
Fire(*Entry) error
}
// Internal type for storing the hooks on a logger instance.
type LevelHooks map[Level][]Hook
// Add a hook to an instance of logger. This is called with
// `log.Hooks.Add(new(MyHook))` where `MyHook` implements the `Hook` interface.
func (hooks LevelHooks) Add(hook Hook) {
for _, level := range hook.Levels() {
hooks[level] = append(hooks[level], hook)
}
}
// Fire all the hooks for the passed level. Used by `entry.log` to fire
// appropriate hooks for a log entry.
func (hooks LevelHooks) Fire(level Level, entry *Entry) error {
for _, hook := range hooks[level] {
if err := hook.Fire(entry); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,54 @@
// +build !windows,!nacl,!plan9
package logrus_syslog
import (
"fmt"
"github.com/Sirupsen/logrus"
"log/syslog"
"os"
)
// SyslogHook to send logs via syslog.
type SyslogHook struct {
Writer *syslog.Writer
SyslogNetwork string
SyslogRaddr string
}
// Creates a hook to be added to an instance of logger. This is called with
// `hook, err := NewSyslogHook("udp", "localhost:514", syslog.LOG_DEBUG, "")`
// `if err == nil { log.Hooks.Add(hook) }`
func NewSyslogHook(network, raddr string, priority syslog.Priority, tag string) (*SyslogHook, error) {
w, err := syslog.Dial(network, raddr, priority, tag)
return &SyslogHook{w, network, raddr}, err
}
func (hook *SyslogHook) Fire(entry *logrus.Entry) error {
line, err := entry.String()
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to read entry, %v", err)
return err
}
switch entry.Level {
case logrus.PanicLevel:
return hook.Writer.Crit(line)
case logrus.FatalLevel:
return hook.Writer.Crit(line)
case logrus.ErrorLevel:
return hook.Writer.Err(line)
case logrus.WarnLevel:
return hook.Writer.Warning(line)
case logrus.InfoLevel:
return hook.Writer.Info(line)
case logrus.DebugLevel:
return hook.Writer.Debug(line)
default:
return nil
}
}
func (hook *SyslogHook) Levels() []logrus.Level {
return logrus.AllLevels
}

67
vendor/github.com/Sirupsen/logrus/hooks/test/test.go generated vendored Normal file
View File

@ -0,0 +1,67 @@
package test
import (
"io/ioutil"
"github.com/Sirupsen/logrus"
)
// test.Hook is a hook designed for dealing with logs in test scenarios.
type Hook struct {
Entries []*logrus.Entry
}
// Installs a test hook for the global logger.
func NewGlobal() *Hook {
hook := new(Hook)
logrus.AddHook(hook)
return hook
}
// Installs a test hook for a given local logger.
func NewLocal(logger *logrus.Logger) *Hook {
hook := new(Hook)
logger.Hooks.Add(hook)
return hook
}
// Creates a discarding logger and installs the test hook.
func NewNullLogger() (*logrus.Logger, *Hook) {
logger := logrus.New()
logger.Out = ioutil.Discard
return logger, NewLocal(logger)
}
func (t *Hook) Fire(e *logrus.Entry) error {
t.Entries = append(t.Entries, e)
return nil
}
func (t *Hook) Levels() []logrus.Level {
return logrus.AllLevels
}
// LastEntry returns the last entry that was logged or nil.
func (t *Hook) LastEntry() (l *logrus.Entry) {
if i := len(t.Entries) - 1; i < 0 {
return nil
} else {
return t.Entries[i]
}
}
// Reset removes all Entries from this test hook.
func (t *Hook) Reset() {
t.Entries = make([]*logrus.Entry, 0)
}

41
vendor/github.com/Sirupsen/logrus/json_formatter.go generated vendored Normal file
View File

@ -0,0 +1,41 @@
package logrus
import (
"encoding/json"
"fmt"
)
type JSONFormatter struct {
// TimestampFormat sets the format used for marshaling timestamps.
TimestampFormat string
}
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields, len(entry.Data)+3)
for k, v := range entry.Data {
switch v := v.(type) {
case error:
// Otherwise errors are ignored by `encoding/json`
// https://github.com/Sirupsen/logrus/issues/137
data[k] = v.Error()
default:
data[k] = v
}
}
prefixFieldClashes(data)
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = DefaultTimestampFormat
}
data["time"] = entry.Time.Format(timestampFormat)
data["msg"] = entry.Message
data["level"] = entry.Level.String()
serialized, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
}
return append(serialized, '\n'), nil
}

212
vendor/github.com/Sirupsen/logrus/logger.go generated vendored Normal file
View File

@ -0,0 +1,212 @@
package logrus
import (
"io"
"os"
"sync"
)
type Logger struct {
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
// file, or leave it default which is `os.Stderr`. You can also set this to
// something more adventorous, such as logging to Kafka.
Out io.Writer
// Hooks for the logger instance. These allow firing events based on logging
// levels and log entries. For example, to send errors to an error tracking
// service, log to StatsD or dump the core on fatal errors.
Hooks LevelHooks
// All log entries pass through the formatter before logged to Out. The
// included formatters are `TextFormatter` and `JSONFormatter` for which
// TextFormatter is the default. In development (when a TTY is attached) it
// logs with colors, but to a file it wouldn't. You can easily implement your
// own that implements the `Formatter` interface, see the `README` or included
// formatters for examples.
Formatter Formatter
// The logging level the logger should log at. This is typically (and defaults
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
// logged. `logrus.Debug` is useful in
Level Level
// Used to sync writing to the log.
mu sync.Mutex
}
// Creates a new logger. Configuration should be set by changing `Formatter`,
// `Out` and `Hooks` directly on the default logger instance. You can also just
// instantiate your own:
//
// var log = &Logger{
// Out: os.Stderr,
// Formatter: new(JSONFormatter),
// Hooks: make(LevelHooks),
// Level: logrus.DebugLevel,
// }
//
// It's recommended to make this a global instance called `log`.
func New() *Logger {
return &Logger{
Out: os.Stderr,
Formatter: new(TextFormatter),
Hooks: make(LevelHooks),
Level: InfoLevel,
}
}
// Adds a field to the log entry, note that you it doesn't log until you call
// Debug, Print, Info, Warn, Fatal or Panic. It only creates a log entry.
// If you want multiple fields, use `WithFields`.
func (logger *Logger) WithField(key string, value interface{}) *Entry {
return NewEntry(logger).WithField(key, value)
}
// Adds a struct of fields to the log entry. All it does is call `WithField` for
// each `Field`.
func (logger *Logger) WithFields(fields Fields) *Entry {
return NewEntry(logger).WithFields(fields)
}
// Add an error as single field to the log entry. All it does is call
// `WithError` for the given `error`.
func (logger *Logger) WithError(err error) *Entry {
return NewEntry(logger).WithError(err)
}
func (logger *Logger) Debugf(format string, args ...interface{}) {
if logger.Level >= DebugLevel {
NewEntry(logger).Debugf(format, args...)
}
}
func (logger *Logger) Infof(format string, args ...interface{}) {
if logger.Level >= InfoLevel {
NewEntry(logger).Infof(format, args...)
}
}
func (logger *Logger) Printf(format string, args ...interface{}) {
NewEntry(logger).Printf(format, args...)
}
func (logger *Logger) Warnf(format string, args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnf(format, args...)
}
}
func (logger *Logger) Warningf(format string, args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnf(format, args...)
}
}
func (logger *Logger) Errorf(format string, args ...interface{}) {
if logger.Level >= ErrorLevel {
NewEntry(logger).Errorf(format, args...)
}
}
func (logger *Logger) Fatalf(format string, args ...interface{}) {
if logger.Level >= FatalLevel {
NewEntry(logger).Fatalf(format, args...)
}
os.Exit(1)
}
func (logger *Logger) Panicf(format string, args ...interface{}) {
if logger.Level >= PanicLevel {
NewEntry(logger).Panicf(format, args...)
}
}
func (logger *Logger) Debug(args ...interface{}) {
if logger.Level >= DebugLevel {
NewEntry(logger).Debug(args...)
}
}
func (logger *Logger) Info(args ...interface{}) {
if logger.Level >= InfoLevel {
NewEntry(logger).Info(args...)
}
}
func (logger *Logger) Print(args ...interface{}) {
NewEntry(logger).Info(args...)
}
func (logger *Logger) Warn(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warn(args...)
}
}
func (logger *Logger) Warning(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warn(args...)
}
}
func (logger *Logger) Error(args ...interface{}) {
if logger.Level >= ErrorLevel {
NewEntry(logger).Error(args...)
}
}
func (logger *Logger) Fatal(args ...interface{}) {
if logger.Level >= FatalLevel {
NewEntry(logger).Fatal(args...)
}
os.Exit(1)
}
func (logger *Logger) Panic(args ...interface{}) {
if logger.Level >= PanicLevel {
NewEntry(logger).Panic(args...)
}
}
func (logger *Logger) Debugln(args ...interface{}) {
if logger.Level >= DebugLevel {
NewEntry(logger).Debugln(args...)
}
}
func (logger *Logger) Infoln(args ...interface{}) {
if logger.Level >= InfoLevel {
NewEntry(logger).Infoln(args...)
}
}
func (logger *Logger) Println(args ...interface{}) {
NewEntry(logger).Println(args...)
}
func (logger *Logger) Warnln(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnln(args...)
}
}
func (logger *Logger) Warningln(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnln(args...)
}
}
func (logger *Logger) Errorln(args ...interface{}) {
if logger.Level >= ErrorLevel {
NewEntry(logger).Errorln(args...)
}
}
func (logger *Logger) Fatalln(args ...interface{}) {
if logger.Level >= FatalLevel {
NewEntry(logger).Fatalln(args...)
}
os.Exit(1)
}
func (logger *Logger) Panicln(args ...interface{}) {
if logger.Level >= PanicLevel {
NewEntry(logger).Panicln(args...)
}
}

143
vendor/github.com/Sirupsen/logrus/logrus.go generated vendored Normal file
View File

@ -0,0 +1,143 @@
package logrus
import (
"fmt"
"log"
"strings"
)
// Fields type, used to pass to `WithFields`.
type Fields map[string]interface{}
// Level type
type Level uint8
// Convert the Level to a string. E.g. PanicLevel becomes "panic".
func (level Level) String() string {
switch level {
case DebugLevel:
return "debug"
case InfoLevel:
return "info"
case WarnLevel:
return "warning"
case ErrorLevel:
return "error"
case FatalLevel:
return "fatal"
case PanicLevel:
return "panic"
}
return "unknown"
}
// ParseLevel takes a string level and returns the Logrus log level constant.
func ParseLevel(lvl string) (Level, error) {
switch strings.ToLower(lvl) {
case "panic":
return PanicLevel, nil
case "fatal":
return FatalLevel, nil
case "error":
return ErrorLevel, nil
case "warn", "warning":
return WarnLevel, nil
case "info":
return InfoLevel, nil
case "debug":
return DebugLevel, nil
}
var l Level
return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
}
// A constant exposing all logging levels
var AllLevels = []Level{
PanicLevel,
FatalLevel,
ErrorLevel,
WarnLevel,
InfoLevel,
DebugLevel,
}
// These are the different logging levels. You can set the logging level to log
// on your instance of logger, obtained with `logrus.New()`.
const (
// PanicLevel level, highest level of severity. Logs and then calls panic with the
// message passed to Debug, Info, ...
PanicLevel Level = iota
// FatalLevel level. Logs and then calls `os.Exit(1)`. It will exit even if the
// logging level is set to Panic.
FatalLevel
// ErrorLevel level. Logs. Used for errors that should definitely be noted.
// Commonly used for hooks to send errors to an error tracking service.
ErrorLevel
// WarnLevel level. Non-critical entries that deserve eyes.
WarnLevel
// InfoLevel level. General operational entries about what's going on inside the
// application.
InfoLevel
// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
DebugLevel
)
// Won't compile if StdLogger can't be realized by a log.Logger
var (
_ StdLogger = &log.Logger{}
_ StdLogger = &Entry{}
_ StdLogger = &Logger{}
)
// StdLogger is what your logrus-enabled library should take, that way
// it'll accept a stdlib logger and a logrus logger. There's no standard
// interface, this is the closest we get, unfortunately.
type StdLogger interface {
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
Fatal(...interface{})
Fatalf(string, ...interface{})
Fatalln(...interface{})
Panic(...interface{})
Panicf(string, ...interface{})
Panicln(...interface{})
}
// The FieldLogger interface generalizes the Entry and Logger types
type FieldLogger interface {
WithField(key string, value interface{}) *Entry
WithFields(fields Fields) *Entry
WithError(err error) *Entry
Debugf(format string, args ...interface{})
Infof(format string, args ...interface{})
Printf(format string, args ...interface{})
Warnf(format string, args ...interface{})
Warningf(format string, args ...interface{})
Errorf(format string, args ...interface{})
Fatalf(format string, args ...interface{})
Panicf(format string, args ...interface{})
Debug(args ...interface{})
Info(args ...interface{})
Print(args ...interface{})
Warn(args ...interface{})
Warning(args ...interface{})
Error(args ...interface{})
Fatal(args ...interface{})
Panic(args ...interface{})
Debugln(args ...interface{})
Infoln(args ...interface{})
Println(args ...interface{})
Warnln(args ...interface{})
Warningln(args ...interface{})
Errorln(args ...interface{})
Fatalln(args ...interface{})
Panicln(args ...interface{})
}

9
vendor/github.com/Sirupsen/logrus/terminal_bsd.go generated vendored Normal file
View File

@ -0,0 +1,9 @@
// +build darwin freebsd openbsd netbsd dragonfly
package logrus
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA
type Termios syscall.Termios

12
vendor/github.com/Sirupsen/logrus/terminal_linux.go generated vendored Normal file
View File

@ -0,0 +1,12 @@
// Based on ssh/terminal:
// Copyright 2013 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 logrus
import "syscall"
const ioctlReadTermios = syscall.TCGETS
type Termios syscall.Termios

View File

@ -0,0 +1,21 @@
// Based on ssh/terminal:
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux darwin freebsd openbsd netbsd dragonfly
package logrus
import (
"syscall"
"unsafe"
)
// IsTerminal returns true if stderr's file descriptor is a terminal.
func IsTerminal() bool {
fd := syscall.Stderr
var termios Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
}

15
vendor/github.com/Sirupsen/logrus/terminal_solaris.go generated vendored Normal file
View File

@ -0,0 +1,15 @@
// +build solaris
package logrus
import (
"os"
"golang.org/x/sys/unix"
)
// IsTerminal returns true if the given file descriptor is a terminal.
func IsTerminal() bool {
_, err := unix.IoctlGetTermios(int(os.Stdout.Fd()), unix.TCGETA)
return err == nil
}

27
vendor/github.com/Sirupsen/logrus/terminal_windows.go generated vendored Normal file
View File

@ -0,0 +1,27 @@
// Based on ssh/terminal:
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build windows
package logrus
import (
"syscall"
"unsafe"
)
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var (
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
)
// IsTerminal returns true if stderr's file descriptor is a terminal.
func IsTerminal() bool {
fd := syscall.Stderr
var st uint32
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
return r != 0 && e == 0
}

161
vendor/github.com/Sirupsen/logrus/text_formatter.go generated vendored Normal file
View File

@ -0,0 +1,161 @@
package logrus
import (
"bytes"
"fmt"
"runtime"
"sort"
"strings"
"time"
)
const (
nocolor = 0
red = 31
green = 32
yellow = 33
blue = 34
gray = 37
)
var (
baseTimestamp time.Time
isTerminal bool
)
func init() {
baseTimestamp = time.Now()
isTerminal = IsTerminal()
}
func miniTS() int {
return int(time.Since(baseTimestamp) / time.Second)
}
type TextFormatter struct {
// Set to true to bypass checking for a TTY before outputting colors.
ForceColors bool
// Force disabling colors.
DisableColors bool
// Disable timestamp logging. useful when output is redirected to logging
// system that already adds timestamps.
DisableTimestamp bool
// Enable logging the full timestamp when a TTY is attached instead of just
// the time passed since beginning of execution.
FullTimestamp bool
// TimestampFormat to use for display when a full timestamp is printed
TimestampFormat string
// The fields are sorted by default for a consistent output. For applications
// that log extremely frequently and don't use the JSON formatter this may not
// be desired.
DisableSorting bool
}
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
var keys []string = make([]string, 0, len(entry.Data))
for k := range entry.Data {
keys = append(keys, k)
}
if !f.DisableSorting {
sort.Strings(keys)
}
b := &bytes.Buffer{}
prefixFieldClashes(entry.Data)
isColorTerminal := isTerminal && (runtime.GOOS != "windows")
isColored := (f.ForceColors || isColorTerminal) && !f.DisableColors
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = DefaultTimestampFormat
}
if isColored {
f.printColored(b, entry, keys, timestampFormat)
} else {
if !f.DisableTimestamp {
f.appendKeyValue(b, "time", entry.Time.Format(timestampFormat))
}
f.appendKeyValue(b, "level", entry.Level.String())
if entry.Message != "" {
f.appendKeyValue(b, "msg", entry.Message)
}
for _, key := range keys {
f.appendKeyValue(b, key, entry.Data[key])
}
}
b.WriteByte('\n')
return b.Bytes(), nil
}
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, timestampFormat string) {
var levelColor int
switch entry.Level {
case DebugLevel:
levelColor = gray
case WarnLevel:
levelColor = yellow
case ErrorLevel, FatalLevel, PanicLevel:
levelColor = red
default:
levelColor = blue
}
levelText := strings.ToUpper(entry.Level.String())[0:4]
if !f.FullTimestamp {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message)
} else {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), entry.Message)
}
for _, k := range keys {
v := entry.Data[k]
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%+v", levelColor, k, v)
}
}
func needsQuoting(text string) bool {
for _, ch := range text {
if !((ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '-' || ch == '.') {
return false
}
}
return true
}
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
b.WriteString(key)
b.WriteByte('=')
switch value := value.(type) {
case string:
if needsQuoting(value) {
b.WriteString(value)
} else {
fmt.Fprintf(b, "%q", value)
}
case error:
errmsg := value.Error()
if needsQuoting(errmsg) {
b.WriteString(errmsg)
} else {
fmt.Fprintf(b, "%q", value)
}
default:
fmt.Fprint(b, value)
}
b.WriteByte(' ')
}

31
vendor/github.com/Sirupsen/logrus/writer.go generated vendored Normal file
View File

@ -0,0 +1,31 @@
package logrus
import (
"bufio"
"io"
"runtime"
)
func (logger *Logger) Writer() *io.PipeWriter {
reader, writer := io.Pipe()
go logger.writerScanner(reader)
runtime.SetFinalizer(writer, writerFinalizer)
return writer
}
func (logger *Logger) writerScanner(reader *io.PipeReader) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
logger.Print(scanner.Text())
}
if err := scanner.Err(); err != nil {
logger.Errorf("Error while reading from Writer: %s", err)
}
reader.Close()
}
func writerFinalizer(writer *io.PipeWriter) {
writer.Close()
}

13
vendor/github.com/alecthomas/log4go/LICENSE generated vendored Normal file
View File

@ -0,0 +1,13 @@
Copyright (c) 2010, Kyle Lemons <kyle@kylelemons.net>. 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.
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.

288
vendor/github.com/alecthomas/log4go/config.go generated vendored Normal file
View File

@ -0,0 +1,288 @@
// Copyright (C) 2010, Kyle Lemons <kyle@kylelemons.net>. All rights reserved.
package log4go
import (
"encoding/xml"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
)
type xmlProperty struct {
Name string `xml:"name,attr"`
Value string `xml:",chardata"`
}
type xmlFilter struct {
Enabled string `xml:"enabled,attr"`
Tag string `xml:"tag"`
Level string `xml:"level"`
Type string `xml:"type"`
Property []xmlProperty `xml:"property"`
}
type xmlLoggerConfig struct {
Filter []xmlFilter `xml:"filter"`
}
// Load XML configuration; see examples/example.xml for documentation
func (log Logger) LoadConfiguration(filename string) {
log.Close()
// Open the configuration file
fd, err := os.Open(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not open %q for reading: %s\n", filename, err)
os.Exit(1)
}
contents, err := ioutil.ReadAll(fd)
if err != nil {
fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not read %q: %s\n", filename, err)
os.Exit(1)
}
xc := new(xmlLoggerConfig)
if err := xml.Unmarshal(contents, xc); err != nil {
fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not parse XML configuration in %q: %s\n", filename, err)
os.Exit(1)
}
for _, xmlfilt := range xc.Filter {
var filt LogWriter
var lvl Level
bad, good, enabled := false, true, false
// Check required children
if len(xmlfilt.Enabled) == 0 {
fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required attribute %s for filter missing in %s\n", "enabled", filename)
bad = true
} else {
enabled = xmlfilt.Enabled != "false"
}
if len(xmlfilt.Tag) == 0 {
fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter missing in %s\n", "tag", filename)
bad = true
}
if len(xmlfilt.Type) == 0 {
fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter missing in %s\n", "type", filename)
bad = true
}
if len(xmlfilt.Level) == 0 {
fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter missing in %s\n", "level", filename)
bad = true
}
switch xmlfilt.Level {
case "FINEST":
lvl = FINEST
case "FINE":
lvl = FINE
case "DEBUG":
lvl = DEBUG
case "TRACE":
lvl = TRACE
case "INFO":
lvl = INFO
case "WARNING":
lvl = WARNING
case "ERROR":
lvl = ERROR
case "CRITICAL":
lvl = CRITICAL
default:
fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter has unknown value in %s: %s\n", "level", filename, xmlfilt.Level)
bad = true
}
// Just so all of the required attributes are errored at the same time if missing
if bad {
os.Exit(1)
}
switch xmlfilt.Type {
case "console":
filt, good = xmlToConsoleLogWriter(filename, xmlfilt.Property, enabled)
case "file":
filt, good = xmlToFileLogWriter(filename, xmlfilt.Property, enabled)
case "xml":
filt, good = xmlToXMLLogWriter(filename, xmlfilt.Property, enabled)
case "socket":
filt, good = xmlToSocketLogWriter(filename, xmlfilt.Property, enabled)
default:
fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not load XML configuration in %s: unknown filter type \"%s\"\n", filename, xmlfilt.Type)
os.Exit(1)
}
// Just so all of the required params are errored at the same time if wrong
if !good {
os.Exit(1)
}
// If we're disabled (syntax and correctness checks only), don't add to logger
if !enabled {
continue
}
log[xmlfilt.Tag] = &Filter{lvl, filt}
}
}
func xmlToConsoleLogWriter(filename string, props []xmlProperty, enabled bool) (*ConsoleLogWriter, bool) {
// Parse properties
for _, prop := range props {
switch prop.Name {
default:
fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for console filter in %s\n", prop.Name, filename)
}
}
// If it's disabled, we're just checking syntax
if !enabled {
return nil, true
}
return NewConsoleLogWriter(), true
}
// Parse a number with K/M/G suffixes based on thousands (1000) or 2^10 (1024)
func strToNumSuffix(str string, mult int) int {
num := 1
if len(str) > 1 {
switch str[len(str)-1] {
case 'G', 'g':
num *= mult
fallthrough
case 'M', 'm':
num *= mult
fallthrough
case 'K', 'k':
num *= mult
str = str[0 : len(str)-1]
}
}
parsed, _ := strconv.Atoi(str)
return parsed * num
}
func xmlToFileLogWriter(filename string, props []xmlProperty, enabled bool) (*FileLogWriter, bool) {
file := ""
format := "[%D %T] [%L] (%S) %M"
maxlines := 0
maxsize := 0
daily := false
rotate := false
// Parse properties
for _, prop := range props {
switch prop.Name {
case "filename":
file = strings.Trim(prop.Value, " \r\n")
case "format":
format = strings.Trim(prop.Value, " \r\n")
case "maxlines":
maxlines = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1000)
case "maxsize":
maxsize = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1024)
case "daily":
daily = strings.Trim(prop.Value, " \r\n") != "false"
case "rotate":
rotate = strings.Trim(prop.Value, " \r\n") != "false"
default:
fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for file filter in %s\n", prop.Name, filename)
}
}
// Check properties
if len(file) == 0 {
fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required property \"%s\" for file filter missing in %s\n", "filename", filename)
return nil, false
}
// If it's disabled, we're just checking syntax
if !enabled {
return nil, true
}
flw := NewFileLogWriter(file, rotate)
flw.SetFormat(format)
flw.SetRotateLines(maxlines)
flw.SetRotateSize(maxsize)
flw.SetRotateDaily(daily)
return flw, true
}
func xmlToXMLLogWriter(filename string, props []xmlProperty, enabled bool) (*FileLogWriter, bool) {
file := ""
maxrecords := 0
maxsize := 0
daily := false
rotate := false
// Parse properties
for _, prop := range props {
switch prop.Name {
case "filename":
file = strings.Trim(prop.Value, " \r\n")
case "maxrecords":
maxrecords = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1000)
case "maxsize":
maxsize = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1024)
case "daily":
daily = strings.Trim(prop.Value, " \r\n") != "false"
case "rotate":
rotate = strings.Trim(prop.Value, " \r\n") != "false"
default:
fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for xml filter in %s\n", prop.Name, filename)
}
}
// Check properties
if len(file) == 0 {
fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required property \"%s\" for xml filter missing in %s\n", "filename", filename)
return nil, false
}
// If it's disabled, we're just checking syntax
if !enabled {
return nil, true
}
xlw := NewXMLLogWriter(file, rotate)
xlw.SetRotateLines(maxrecords)
xlw.SetRotateSize(maxsize)
xlw.SetRotateDaily(daily)
return xlw, true
}
func xmlToSocketLogWriter(filename string, props []xmlProperty, enabled bool) (SocketLogWriter, bool) {
endpoint := ""
protocol := "udp"
// Parse properties
for _, prop := range props {
switch prop.Name {
case "endpoint":
endpoint = strings.Trim(prop.Value, " \r\n")
case "protocol":
protocol = strings.Trim(prop.Value, " \r\n")
default:
fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for file filter in %s\n", prop.Name, filename)
}
}
// Check properties
if len(endpoint) == 0 {
fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required property \"%s\" for file filter missing in %s\n", "endpoint", filename)
return nil, false
}
// If it's disabled, we're just checking syntax
if !enabled {
return nil, true
}
return NewSocketLogWriter(protocol, endpoint), true
}

View File

@ -0,0 +1,14 @@
package main
import (
"time"
)
import l4g "code.google.com/p/log4go"
func main() {
log := l4g.NewLogger()
defer log.Close()
log.AddFilter("stdout", l4g.DEBUG, l4g.NewConsoleLogWriter())
log.Info("The time is now: %s", time.Now().Format("15:04:05 MST 2006/01/02"))
}

View File

@ -0,0 +1,57 @@
package main
import (
"bufio"
"fmt"
"io"
"os"
"time"
)
import l4g "code.google.com/p/log4go"
const (
filename = "flw.log"
)
func main() {
// Get a new logger instance
log := l4g.NewLogger()
// Create a default logger that is logging messages of FINE or higher
log.AddFilter("file", l4g.FINE, l4g.NewFileLogWriter(filename, false))
log.Close()
/* Can also specify manually via the following: (these are the defaults) */
flw := l4g.NewFileLogWriter(filename, false)
flw.SetFormat("[%D %T] [%L] (%S) %M")
flw.SetRotate(false)
flw.SetRotateSize(0)
flw.SetRotateLines(0)
flw.SetRotateDaily(false)
log.AddFilter("file", l4g.FINE, flw)
// Log some experimental messages
log.Finest("Everything is created now (notice that I will not be printing to the file)")
log.Info("The time is now: %s", time.Now().Format("15:04:05 MST 2006/01/02"))
log.Critical("Time to close out!")
// Close the log
log.Close()
// Print what was logged to the file (yes, I know I'm skipping error checking)
fd, _ := os.Open(filename)
in := bufio.NewReader(fd)
fmt.Print("Messages logged to file were: (line numbers not included)\n")
for lineno := 1; ; lineno++ {
line, err := in.ReadString('\n')
if err == io.EOF {
break
}
fmt.Printf("%3d:\t%s", lineno, line)
}
fd.Close()
// Remove the file so it's not lying around
os.Remove(filename)
}

View File

@ -0,0 +1,42 @@
package main
import (
"flag"
"fmt"
"net"
"os"
)
var (
port = flag.String("p", "12124", "Port number to listen on")
)
func e(err error) {
if err != nil {
fmt.Printf("Erroring out: %s\n", err)
os.Exit(1)
}
}
func main() {
flag.Parse()
// Bind to the port
bind, err := net.ResolveUDPAddr("0.0.0.0:" + *port)
e(err)
// Create listener
listener, err := net.ListenUDP("udp", bind)
e(err)
fmt.Printf("Listening to port %s...\n", *port)
for {
// read into a new buffer
buffer := make([]byte, 1024)
_, _, err := listener.ReadFrom(buffer)
e(err)
// log to standard output
fmt.Println(string(buffer))
}
}

View File

@ -0,0 +1,18 @@
package main
import (
"time"
)
import l4g "code.google.com/p/log4go"
func main() {
log := l4g.NewLogger()
log.AddFilter("network", l4g.FINEST, l4g.NewSocketLogWriter("udp", "192.168.1.255:12124"))
// Run `nc -u -l -p 12124` or similar before you run this to see the following message
log.Info("The time is now: %s", time.Now().Format("15:04:05 MST 2006/01/02"))
// This makes sure the output stream buffer is written
log.Close()
}

View File

@ -0,0 +1,13 @@
package main
import l4g "code.google.com/p/log4go"
func main() {
// Load the configuration (isn't this easy?)
l4g.LoadConfiguration("example.xml")
// And now we're ready!
l4g.Finest("This will only go to those of you really cool UDP kids! If you change enabled=true.")
l4g.Debug("Oh no! %d + %d = %d!", 2, 2, 2+2)
l4g.Info("About that time, eh chaps?")
}

264
vendor/github.com/alecthomas/log4go/filelog.go generated vendored Normal file
View File

@ -0,0 +1,264 @@
// Copyright (C) 2010, Kyle Lemons <kyle@kylelemons.net>. All rights reserved.
package log4go
import (
"fmt"
"os"
"time"
)
// This log writer sends output to a file
type FileLogWriter struct {
rec chan *LogRecord
rot chan bool
// The opened file
filename string
file *os.File
// The logging format
format string
// File header/trailer
header, trailer string
// Rotate at linecount
maxlines int
maxlines_curlines int
// Rotate at size
maxsize int
maxsize_cursize int
// Rotate daily
daily bool
daily_opendate int
// Keep old logfiles (.001, .002, etc)
rotate bool
maxbackup int
}
// This is the FileLogWriter's output method
func (w *FileLogWriter) LogWrite(rec *LogRecord) {
w.rec <- rec
}
func (w *FileLogWriter) Close() {
close(w.rec)
w.file.Sync()
}
// NewFileLogWriter creates a new LogWriter which writes to the given file and
// has rotation enabled if rotate is true.
//
// If rotate is true, any time a new log file is opened, the old one is renamed
// with a .### extension to preserve it. The various Set* methods can be used
// to configure log rotation based on lines, size, and daily.
//
// The standard log-line format is:
// [%D %T] [%L] (%S) %M
func NewFileLogWriter(fname string, rotate bool) *FileLogWriter {
w := &FileLogWriter{
rec: make(chan *LogRecord, LogBufferLength),
rot: make(chan bool),
filename: fname,
format: "[%D %T] [%L] (%S) %M",
rotate: rotate,
maxbackup: 999,
}
// open the file for the first time
if err := w.intRotate(); err != nil {
fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.filename, err)
return nil
}
go func() {
defer func() {
if w.file != nil {
fmt.Fprint(w.file, FormatLogRecord(w.trailer, &LogRecord{Created: time.Now()}))
w.file.Close()
}
}()
for {
select {
case <-w.rot:
if err := w.intRotate(); err != nil {
fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.filename, err)
return
}
case rec, ok := <-w.rec:
if !ok {
return
}
now := time.Now()
if (w.maxlines > 0 && w.maxlines_curlines >= w.maxlines) ||
(w.maxsize > 0 && w.maxsize_cursize >= w.maxsize) ||
(w.daily && now.Day() != w.daily_opendate) {
if err := w.intRotate(); err != nil {
fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.filename, err)
return
}
}
// Perform the write
n, err := fmt.Fprint(w.file, FormatLogRecord(w.format, rec))
if err != nil {
fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.filename, err)
return
}
// Update the counts
w.maxlines_curlines++
w.maxsize_cursize += n
}
}
}()
return w
}
// Request that the logs rotate
func (w *FileLogWriter) Rotate() {
w.rot <- true
}
// If this is called in a threaded context, it MUST be synchronized
func (w *FileLogWriter) intRotate() error {
// Close any log file that may be open
if w.file != nil {
fmt.Fprint(w.file, FormatLogRecord(w.trailer, &LogRecord{Created: time.Now()}))
w.file.Close()
}
// If we are keeping log files, move it to the next available number
if w.rotate {
_, err := os.Lstat(w.filename)
if err == nil { // file exists
// Find the next available number
num := 1
fname := ""
if w.daily && time.Now().Day() != w.daily_opendate {
yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02")
for ; err == nil && num <= 999; num++ {
fname = w.filename + fmt.Sprintf(".%s.%03d", yesterday, num)
_, err = os.Lstat(fname)
}
// return error if the last file checked still existed
if err == nil {
return fmt.Errorf("Rotate: Cannot find free log number to rename %s\n", w.filename)
}
} else {
num = w.maxbackup - 1
for ; num >= 1; num-- {
fname = w.filename + fmt.Sprintf(".%d", num)
nfname := w.filename + fmt.Sprintf(".%d", num+1)
_, err = os.Lstat(fname)
if err == nil {
os.Rename(fname, nfname)
}
}
}
w.file.Close()
// Rename the file to its newfound home
err = os.Rename(w.filename, fname)
if err != nil {
return fmt.Errorf("Rotate: %s\n", err)
}
}
}
// Open the log file
fd, err := os.OpenFile(w.filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0660)
if err != nil {
return err
}
w.file = fd
now := time.Now()
fmt.Fprint(w.file, FormatLogRecord(w.header, &LogRecord{Created: now}))
// Set the daily open date to the current date
w.daily_opendate = now.Day()
// initialize rotation values
w.maxlines_curlines = 0
w.maxsize_cursize = 0
return nil
}
// Set the logging format (chainable). Must be called before the first log
// message is written.
func (w *FileLogWriter) SetFormat(format string) *FileLogWriter {
w.format = format
return w
}
// Set the logfile header and footer (chainable). Must be called before the first log
// message is written. These are formatted similar to the FormatLogRecord (e.g.
// you can use %D and %T in your header/footer for date and time).
func (w *FileLogWriter) SetHeadFoot(head, foot string) *FileLogWriter {
w.header, w.trailer = head, foot
if w.maxlines_curlines == 0 {
fmt.Fprint(w.file, FormatLogRecord(w.header, &LogRecord{Created: time.Now()}))
}
return w
}
// Set rotate at linecount (chainable). Must be called before the first log
// message is written.
func (w *FileLogWriter) SetRotateLines(maxlines int) *FileLogWriter {
//fmt.Fprintf(os.Stderr, "FileLogWriter.SetRotateLines: %v\n", maxlines)
w.maxlines = maxlines
return w
}
// Set rotate at size (chainable). Must be called before the first log message
// is written.
func (w *FileLogWriter) SetRotateSize(maxsize int) *FileLogWriter {
//fmt.Fprintf(os.Stderr, "FileLogWriter.SetRotateSize: %v\n", maxsize)
w.maxsize = maxsize
return w
}
// Set rotate daily (chainable). Must be called before the first log message is
// written.
func (w *FileLogWriter) SetRotateDaily(daily bool) *FileLogWriter {
//fmt.Fprintf(os.Stderr, "FileLogWriter.SetRotateDaily: %v\n", daily)
w.daily = daily
return w
}
// Set max backup files. Must be called before the first log message
// is written.
func (w *FileLogWriter) SetRotateMaxBackup(maxbackup int) *FileLogWriter {
w.maxbackup = maxbackup
return w
}
// SetRotate changes whether or not the old logs are kept. (chainable) Must be
// called before the first log message is written. If rotate is false, the
// files are overwritten; otherwise, they are rotated to another file before the
// new log is opened.
func (w *FileLogWriter) SetRotate(rotate bool) *FileLogWriter {
//fmt.Fprintf(os.Stderr, "FileLogWriter.SetRotate: %v\n", rotate)
w.rotate = rotate
return w
}
// NewXMLLogWriter is a utility method for creating a FileLogWriter set up to
// output XML record log messages instead of line-based ones.
func NewXMLLogWriter(fname string, rotate bool) *FileLogWriter {
return NewFileLogWriter(fname, rotate).SetFormat(
` <record level="%L">
<timestamp>%D %T</timestamp>
<source>%S</source>
<message>%M</message>
</record>`).SetHeadFoot("<log created=\"%D %T\">", "</log>")
}

484
vendor/github.com/alecthomas/log4go/log4go.go generated vendored Normal file
View File

@ -0,0 +1,484 @@
// Copyright (C) 2010, Kyle Lemons <kyle@kylelemons.net>. All rights reserved.
// Package log4go provides level-based and highly configurable logging.
//
// Enhanced Logging
//
// This is inspired by the logging functionality in Java. Essentially, you create a Logger
// object and create output filters for it. You can send whatever you want to the Logger,
// and it will filter that based on your settings and send it to the outputs. This way, you
// can put as much debug code in your program as you want, and when you're done you can filter
// out the mundane messages so only the important ones show up.
//
// Utility functions are provided to make life easier. Here is some example code to get started:
//
// log := log4go.NewLogger()
// log.AddFilter("stdout", log4go.DEBUG, log4go.NewConsoleLogWriter())
// log.AddFilter("log", log4go.FINE, log4go.NewFileLogWriter("example.log", true))
// log.Info("The time is now: %s", time.LocalTime().Format("15:04:05 MST 2006/01/02"))
//
// The first two lines can be combined with the utility NewDefaultLogger:
//
// log := log4go.NewDefaultLogger(log4go.DEBUG)
// log.AddFilter("log", log4go.FINE, log4go.NewFileLogWriter("example.log", true))
// log.Info("The time is now: %s", time.LocalTime().Format("15:04:05 MST 2006/01/02"))
//
// Usage notes:
// - The ConsoleLogWriter does not display the source of the message to standard
// output, but the FileLogWriter does.
// - The utility functions (Info, Debug, Warn, etc) derive their source from the
// calling function, and this incurs extra overhead.
//
// Changes from 2.0:
// - The external interface has remained mostly stable, but a lot of the
// internals have been changed, so if you depended on any of this or created
// your own LogWriter, then you will probably have to update your code. In
// particular, Logger is now a map and ConsoleLogWriter is now a channel
// behind-the-scenes, and the LogWrite method no longer has return values.
//
// Future work: (please let me know if you think I should work on any of these particularly)
// - Log file rotation
// - Logging configuration files ala log4j
// - Have the ability to remove filters?
// - Have GetInfoChannel, GetDebugChannel, etc return a chan string that allows
// for another method of logging
// - Add an XML filter type
package log4go
import (
"errors"
"fmt"
"os"
"runtime"
"strings"
"time"
)
// Version information
const (
L4G_VERSION = "log4go-v3.0.1"
L4G_MAJOR = 3
L4G_MINOR = 0
L4G_BUILD = 1
)
/****** Constants ******/
// These are the integer logging levels used by the logger
type Level int
const (
FINEST Level = iota
FINE
DEBUG
TRACE
INFO
WARNING
ERROR
CRITICAL
)
// Logging level strings
var (
levelStrings = [...]string{"FNST", "FINE", "DEBG", "TRAC", "INFO", "WARN", "EROR", "CRIT"}
)
func (l Level) String() string {
if l < 0 || int(l) > len(levelStrings) {
return "UNKNOWN"
}
return levelStrings[int(l)]
}
/****** Variables ******/
var (
// LogBufferLength specifies how many log messages a particular log4go
// logger can buffer at a time before writing them.
LogBufferLength = 32
)
/****** LogRecord ******/
// A LogRecord contains all of the pertinent information for each message
type LogRecord struct {
Level Level // The log level
Created time.Time // The time at which the log message was created (nanoseconds)
Source string // The message source
Message string // The log message
}
/****** LogWriter ******/
// This is an interface for anything that should be able to write logs
type LogWriter interface {
// This will be called to log a LogRecord message.
LogWrite(rec *LogRecord)
// This should clean up anything lingering about the LogWriter, as it is called before
// the LogWriter is removed. LogWrite should not be called after Close.
Close()
}
/****** Logger ******/
// A Filter represents the log level below which no log records are written to
// the associated LogWriter.
type Filter struct {
Level Level
LogWriter
}
// A Logger represents a collection of Filters through which log messages are
// written.
type Logger map[string]*Filter
// Create a new logger.
//
// DEPRECATED: Use make(Logger) instead.
func NewLogger() Logger {
os.Stderr.WriteString("warning: use of deprecated NewLogger\n")
return make(Logger)
}
// Create a new logger with a "stdout" filter configured to send log messages at
// or above lvl to standard output.
//
// DEPRECATED: use NewDefaultLogger instead.
func NewConsoleLogger(lvl Level) Logger {
os.Stderr.WriteString("warning: use of deprecated NewConsoleLogger\n")
return Logger{
"stdout": &Filter{lvl, NewConsoleLogWriter()},
}
}
// Create a new logger with a "stdout" filter configured to send log messages at
// or above lvl to standard output.
func NewDefaultLogger(lvl Level) Logger {
return Logger{
"stdout": &Filter{lvl, NewConsoleLogWriter()},
}
}
// Closes all log writers in preparation for exiting the program or a
// reconfiguration of logging. Calling this is not really imperative, unless
// you want to guarantee that all log messages are written. Close removes
// all filters (and thus all LogWriters) from the logger.
func (log Logger) Close() {
// Close all open loggers
for name, filt := range log {
filt.Close()
delete(log, name)
}
}
// Add a new LogWriter to the Logger which will only log messages at lvl or
// higher. This function should not be called from multiple goroutines.
// Returns the logger for chaining.
func (log Logger) AddFilter(name string, lvl Level, writer LogWriter) Logger {
log[name] = &Filter{lvl, writer}
return log
}
/******* Logging *******/
// Send a formatted log message internally
func (log Logger) intLogf(lvl Level, format string, args ...interface{}) {
skip := true
// Determine if any logging will be done
for _, filt := range log {
if lvl >= filt.Level {
skip = false
break
}
}
if skip {
return
}
// Determine caller func
pc, _, lineno, ok := runtime.Caller(2)
src := ""
if ok {
src = fmt.Sprintf("%s:%d", runtime.FuncForPC(pc).Name(), lineno)
}
msg := format
if len(args) > 0 {
msg = fmt.Sprintf(format, args...)
}
// Make the log record
rec := &LogRecord{
Level: lvl,
Created: time.Now(),
Source: src,
Message: msg,
}
// Dispatch the logs
for _, filt := range log {
if lvl < filt.Level {
continue
}
filt.LogWrite(rec)
}
}
// Send a closure log message internally
func (log Logger) intLogc(lvl Level, closure func() string) {
skip := true
// Determine if any logging will be done
for _, filt := range log {
if lvl >= filt.Level {
skip = false
break
}
}
if skip {
return
}
// Determine caller func
pc, _, lineno, ok := runtime.Caller(2)
src := ""
if ok {
src = fmt.Sprintf("%s:%d", runtime.FuncForPC(pc).Name(), lineno)
}
// Make the log record
rec := &LogRecord{
Level: lvl,
Created: time.Now(),
Source: src,
Message: closure(),
}
// Dispatch the logs
for _, filt := range log {
if lvl < filt.Level {
continue
}
filt.LogWrite(rec)
}
}
// Send a log message with manual level, source, and message.
func (log Logger) Log(lvl Level, source, message string) {
skip := true
// Determine if any logging will be done
for _, filt := range log {
if lvl >= filt.Level {
skip = false
break
}
}
if skip {
return
}
// Make the log record
rec := &LogRecord{
Level: lvl,
Created: time.Now(),
Source: source,
Message: message,
}
// Dispatch the logs
for _, filt := range log {
if lvl < filt.Level {
continue
}
filt.LogWrite(rec)
}
}
// Logf logs a formatted log message at the given log level, using the caller as
// its source.
func (log Logger) Logf(lvl Level, format string, args ...interface{}) {
log.intLogf(lvl, format, args...)
}
// Logc logs a string returned by the closure at the given log level, using the caller as
// its source. If no log message would be written, the closure is never called.
func (log Logger) Logc(lvl Level, closure func() string) {
log.intLogc(lvl, closure)
}
// Finest logs a message at the finest log level.
// See Debug for an explanation of the arguments.
func (log Logger) Finest(arg0 interface{}, args ...interface{}) {
const (
lvl = FINEST
)
switch first := arg0.(type) {
case string:
// Use the string as a format string
log.intLogf(lvl, first, args...)
case func() string:
// Log the closure (no other arguments used)
log.intLogc(lvl, first)
default:
// Build a format string so that it will be similar to Sprint
log.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...)
}
}
// Fine logs a message at the fine log level.
// See Debug for an explanation of the arguments.
func (log Logger) Fine(arg0 interface{}, args ...interface{}) {
const (
lvl = FINE
)
switch first := arg0.(type) {
case string:
// Use the string as a format string
log.intLogf(lvl, first, args...)
case func() string:
// Log the closure (no other arguments used)
log.intLogc(lvl, first)
default:
// Build a format string so that it will be similar to Sprint
log.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...)
}
}
// Debug is a utility method for debug log messages.
// The behavior of Debug depends on the first argument:
// - arg0 is a string
// When given a string as the first argument, this behaves like Logf but with
// the DEBUG log level: the first argument is interpreted as a format for the
// latter arguments.
// - arg0 is a func()string
// When given a closure of type func()string, this logs the string returned by
// the closure iff it will be logged. The closure runs at most one time.
// - arg0 is interface{}
// When given anything else, the log message will be each of the arguments
// formatted with %v and separated by spaces (ala Sprint).
func (log Logger) Debug(arg0 interface{}, args ...interface{}) {
const (
lvl = DEBUG
)
switch first := arg0.(type) {
case string:
// Use the string as a format string
log.intLogf(lvl, first, args...)
case func() string:
// Log the closure (no other arguments used)
log.intLogc(lvl, first)
default:
// Build a format string so that it will be similar to Sprint
log.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...)
}
}
// Trace logs a message at the trace log level.
// See Debug for an explanation of the arguments.
func (log Logger) Trace(arg0 interface{}, args ...interface{}) {
const (
lvl = TRACE
)
switch first := arg0.(type) {
case string:
// Use the string as a format string
log.intLogf(lvl, first, args...)
case func() string:
// Log the closure (no other arguments used)
log.intLogc(lvl, first)
default:
// Build a format string so that it will be similar to Sprint
log.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...)
}
}
// Info logs a message at the info log level.
// See Debug for an explanation of the arguments.
func (log Logger) Info(arg0 interface{}, args ...interface{}) {
const (
lvl = INFO
)
switch first := arg0.(type) {
case string:
// Use the string as a format string
log.intLogf(lvl, first, args...)
case func() string:
// Log the closure (no other arguments used)
log.intLogc(lvl, first)
default:
// Build a format string so that it will be similar to Sprint
log.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...)
}
}
// Warn logs a message at the warning log level and returns the formatted error.
// At the warning level and higher, there is no performance benefit if the
// message is not actually logged, because all formats are processed and all
// closures are executed to format the error message.
// See Debug for further explanation of the arguments.
func (log Logger) Warn(arg0 interface{}, args ...interface{}) error {
const (
lvl = WARNING
)
var msg string
switch first := arg0.(type) {
case string:
// Use the string as a format string
msg = fmt.Sprintf(first, args...)
case func() string:
// Log the closure (no other arguments used)
msg = first()
default:
// Build a format string so that it will be similar to Sprint
msg = fmt.Sprintf(fmt.Sprint(first)+strings.Repeat(" %v", len(args)), args...)
}
log.intLogf(lvl, msg)
return errors.New(msg)
}
// Error logs a message at the error log level and returns the formatted error,
// See Warn for an explanation of the performance and Debug for an explanation
// of the parameters.
func (log Logger) Error(arg0 interface{}, args ...interface{}) error {
const (
lvl = ERROR
)
var msg string
switch first := arg0.(type) {
case string:
// Use the string as a format string
msg = fmt.Sprintf(first, args...)
case func() string:
// Log the closure (no other arguments used)
msg = first()
default:
// Build a format string so that it will be similar to Sprint
msg = fmt.Sprintf(fmt.Sprint(first)+strings.Repeat(" %v", len(args)), args...)
}
log.intLogf(lvl, msg)
return errors.New(msg)
}
// Critical logs a message at the critical log level and returns the formatted error,
// See Warn for an explanation of the performance and Debug for an explanation
// of the parameters.
func (log Logger) Critical(arg0 interface{}, args ...interface{}) error {
const (
lvl = CRITICAL
)
var msg string
switch first := arg0.(type) {
case string:
// Use the string as a format string
msg = fmt.Sprintf(first, args...)
case func() string:
// Log the closure (no other arguments used)
msg = first()
default:
// Build a format string so that it will be similar to Sprint
msg = fmt.Sprintf(fmt.Sprint(first)+strings.Repeat(" %v", len(args)), args...)
}
log.intLogf(lvl, msg)
return errors.New(msg)
}

126
vendor/github.com/alecthomas/log4go/pattlog.go generated vendored Normal file
View File

@ -0,0 +1,126 @@
// Copyright (C) 2010, Kyle Lemons <kyle@kylelemons.net>. All rights reserved.
package log4go
import (
"bytes"
"fmt"
"io"
"strings"
)
const (
FORMAT_DEFAULT = "[%D %T] [%L] (%S) %M"
FORMAT_SHORT = "[%t %d] [%L] %M"
FORMAT_ABBREV = "[%L] %M"
)
type formatCacheType struct {
LastUpdateSeconds int64
shortTime, shortDate string
longTime, longDate string
}
var formatCache = &formatCacheType{}
// Known format codes:
// %T - Time (15:04:05 MST)
// %t - Time (15:04)
// %D - Date (2006/01/02)
// %d - Date (01/02/06)
// %L - Level (FNST, FINE, DEBG, TRAC, WARN, EROR, CRIT)
// %S - Source
// %M - Message
// Ignores unknown formats
// Recommended: "[%D %T] [%L] (%S) %M"
func FormatLogRecord(format string, rec *LogRecord) string {
if rec == nil {
return "<nil>"
}
if len(format) == 0 {
return ""
}
out := bytes.NewBuffer(make([]byte, 0, 64))
secs := rec.Created.UnixNano() / 1e9
cache := *formatCache
if cache.LastUpdateSeconds != secs {
month, day, year := rec.Created.Month(), rec.Created.Day(), rec.Created.Year()
hour, minute, second := rec.Created.Hour(), rec.Created.Minute(), rec.Created.Second()
zone, _ := rec.Created.Zone()
updated := &formatCacheType{
LastUpdateSeconds: secs,
shortTime: fmt.Sprintf("%02d:%02d", hour, minute),
shortDate: fmt.Sprintf("%02d/%02d/%02d", day, month, year%100),
longTime: fmt.Sprintf("%02d:%02d:%02d %s", hour, minute, second, zone),
longDate: fmt.Sprintf("%04d/%02d/%02d", year, month, day),
}
cache = *updated
formatCache = updated
}
// Split the string into pieces by % signs
pieces := bytes.Split([]byte(format), []byte{'%'})
// Iterate over the pieces, replacing known formats
for i, piece := range pieces {
if i > 0 && len(piece) > 0 {
switch piece[0] {
case 'T':
out.WriteString(cache.longTime)
case 't':
out.WriteString(cache.shortTime)
case 'D':
out.WriteString(cache.longDate)
case 'd':
out.WriteString(cache.shortDate)
case 'L':
out.WriteString(levelStrings[rec.Level])
case 'S':
out.WriteString(rec.Source)
case 's':
slice := strings.Split(rec.Source, "/")
out.WriteString(slice[len(slice)-1])
case 'M':
out.WriteString(rec.Message)
}
if len(piece) > 1 {
out.Write(piece[1:])
}
} else if len(piece) > 0 {
out.Write(piece)
}
}
out.WriteByte('\n')
return out.String()
}
// This is the standard writer that prints to standard output.
type FormatLogWriter chan *LogRecord
// This creates a new FormatLogWriter
func NewFormatLogWriter(out io.Writer, format string) FormatLogWriter {
records := make(FormatLogWriter, LogBufferLength)
go records.run(out, format)
return records
}
func (w FormatLogWriter) run(out io.Writer, format string) {
for rec := range w {
fmt.Fprint(out, FormatLogRecord(format, rec))
}
}
// This is the FormatLogWriter's output method. This will block if the output
// buffer is full.
func (w FormatLogWriter) LogWrite(rec *LogRecord) {
w <- rec
}
// Close stops the logger from sending messages to standard output. Attempts to
// send log messages to this logger after a Close have undefined behavior.
func (w FormatLogWriter) Close() {
close(w)
}

57
vendor/github.com/alecthomas/log4go/socklog.go generated vendored Normal file
View File

@ -0,0 +1,57 @@
// Copyright (C) 2010, Kyle Lemons <kyle@kylelemons.net>. All rights reserved.
package log4go
import (
"encoding/json"
"fmt"
"net"
"os"
)
// This log writer sends output to a socket
type SocketLogWriter chan *LogRecord
// This is the SocketLogWriter's output method
func (w SocketLogWriter) LogWrite(rec *LogRecord) {
w <- rec
}
func (w SocketLogWriter) Close() {
close(w)
}
func NewSocketLogWriter(proto, hostport string) SocketLogWriter {
sock, err := net.Dial(proto, hostport)
if err != nil {
fmt.Fprintf(os.Stderr, "NewSocketLogWriter(%q): %s\n", hostport, err)
return nil
}
w := SocketLogWriter(make(chan *LogRecord, LogBufferLength))
go func() {
defer func() {
if sock != nil && proto == "tcp" {
sock.Close()
}
}()
for rec := range w {
// Marshall into JSON
js, err := json.Marshal(rec)
if err != nil {
fmt.Fprint(os.Stderr, "SocketLogWriter(%q): %s", hostport, err)
return
}
_, err = sock.Write(js)
if err != nil {
fmt.Fprint(os.Stderr, "SocketLogWriter(%q): %s", hostport, err)
return
}
}
}()
return w
}

49
vendor/github.com/alecthomas/log4go/termlog.go generated vendored Normal file
View File

@ -0,0 +1,49 @@
// Copyright (C) 2010, Kyle Lemons <kyle@kylelemons.net>. All rights reserved.
package log4go
import (
"fmt"
"io"
"os"
"time"
)
var stdout io.Writer = os.Stdout
// This is the standard writer that prints to standard output.
type ConsoleLogWriter struct {
format string
w chan *LogRecord
}
// This creates a new ConsoleLogWriter
func NewConsoleLogWriter() *ConsoleLogWriter {
consoleWriter := &ConsoleLogWriter{
format: "[%T %D] [%L] (%S) %M",
w: make(chan *LogRecord, LogBufferLength),
}
go consoleWriter.run(stdout)
return consoleWriter
}
func (c *ConsoleLogWriter) SetFormat(format string) {
c.format = format
}
func (c *ConsoleLogWriter) run(out io.Writer) {
for rec := range c.w {
fmt.Fprint(out, FormatLogRecord(c.format, rec))
}
}
// This is the ConsoleLogWriter's output method. This will block if the output
// buffer is full.
func (c *ConsoleLogWriter) LogWrite(rec *LogRecord) {
c.w <- rec
}
// Close stops the logger from sending messages to standard output. Attempts to
// send log messages to this logger after a Close have undefined behavior.
func (c *ConsoleLogWriter) Close() {
close(c.w)
time.Sleep(50 * time.Millisecond) // Try to give console I/O time to complete
}

278
vendor/github.com/alecthomas/log4go/wrapper.go generated vendored Normal file
View File

@ -0,0 +1,278 @@
// Copyright (C) 2010, Kyle Lemons <kyle@kylelemons.net>. All rights reserved.
package log4go
import (
"errors"
"fmt"
"os"
"strings"
)
var (
Global Logger
)
func init() {
Global = NewDefaultLogger(DEBUG)
}
// Wrapper for (*Logger).LoadConfiguration
func LoadConfiguration(filename string) {
Global.LoadConfiguration(filename)
}
// Wrapper for (*Logger).AddFilter
func AddFilter(name string, lvl Level, writer LogWriter) {
Global.AddFilter(name, lvl, writer)
}
// Wrapper for (*Logger).Close (closes and removes all logwriters)
func Close() {
Global.Close()
}
func Crash(args ...interface{}) {
if len(args) > 0 {
Global.intLogf(CRITICAL, strings.Repeat(" %v", len(args))[1:], args...)
}
panic(args)
}
// Logs the given message and crashes the program
func Crashf(format string, args ...interface{}) {
Global.intLogf(CRITICAL, format, args...)
Global.Close() // so that hopefully the messages get logged
panic(fmt.Sprintf(format, args...))
}
// Compatibility with `log`
func Exit(args ...interface{}) {
if len(args) > 0 {
Global.intLogf(ERROR, strings.Repeat(" %v", len(args))[1:], args...)
}
Global.Close() // so that hopefully the messages get logged
os.Exit(0)
}
// Compatibility with `log`
func Exitf(format string, args ...interface{}) {
Global.intLogf(ERROR, format, args...)
Global.Close() // so that hopefully the messages get logged
os.Exit(0)
}
// Compatibility with `log`
func Stderr(args ...interface{}) {
if len(args) > 0 {
Global.intLogf(ERROR, strings.Repeat(" %v", len(args))[1:], args...)
}
}
// Compatibility with `log`
func Stderrf(format string, args ...interface{}) {
Global.intLogf(ERROR, format, args...)
}
// Compatibility with `log`
func Stdout(args ...interface{}) {
if len(args) > 0 {
Global.intLogf(INFO, strings.Repeat(" %v", len(args))[1:], args...)
}
}
// Compatibility with `log`
func Stdoutf(format string, args ...interface{}) {
Global.intLogf(INFO, format, args...)
}
// Send a log message manually
// Wrapper for (*Logger).Log
func Log(lvl Level, source, message string) {
Global.Log(lvl, source, message)
}
// Send a formatted log message easily
// Wrapper for (*Logger).Logf
func Logf(lvl Level, format string, args ...interface{}) {
Global.intLogf(lvl, format, args...)
}
// Send a closure log message
// Wrapper for (*Logger).Logc
func Logc(lvl Level, closure func() string) {
Global.intLogc(lvl, closure)
}
// Utility for finest log messages (see Debug() for parameter explanation)
// Wrapper for (*Logger).Finest
func Finest(arg0 interface{}, args ...interface{}) {
const (
lvl = FINEST
)
switch first := arg0.(type) {
case string:
// Use the string as a format string
Global.intLogf(lvl, first, args...)
case func() string:
// Log the closure (no other arguments used)
Global.intLogc(lvl, first)
default:
// Build a format string so that it will be similar to Sprint
Global.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...)
}
}
// Utility for fine log messages (see Debug() for parameter explanation)
// Wrapper for (*Logger).Fine
func Fine(arg0 interface{}, args ...interface{}) {
const (
lvl = FINE
)
switch first := arg0.(type) {
case string:
// Use the string as a format string
Global.intLogf(lvl, first, args...)
case func() string:
// Log the closure (no other arguments used)
Global.intLogc(lvl, first)
default:
// Build a format string so that it will be similar to Sprint
Global.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...)
}
}
// Utility for debug log messages
// When given a string as the first argument, this behaves like Logf but with the DEBUG log level (e.g. the first argument is interpreted as a format for the latter arguments)
// When given a closure of type func()string, this logs the string returned by the closure iff it will be logged. The closure runs at most one time.
// When given anything else, the log message will be each of the arguments formatted with %v and separated by spaces (ala Sprint).
// Wrapper for (*Logger).Debug
func Debug(arg0 interface{}, args ...interface{}) {
const (
lvl = DEBUG
)
switch first := arg0.(type) {
case string:
// Use the string as a format string
Global.intLogf(lvl, first, args...)
case func() string:
// Log the closure (no other arguments used)
Global.intLogc(lvl, first)
default:
// Build a format string so that it will be similar to Sprint
Global.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...)
}
}
// Utility for trace log messages (see Debug() for parameter explanation)
// Wrapper for (*Logger).Trace
func Trace(arg0 interface{}, args ...interface{}) {
const (
lvl = TRACE
)
switch first := arg0.(type) {
case string:
// Use the string as a format string
Global.intLogf(lvl, first, args...)
case func() string:
// Log the closure (no other arguments used)
Global.intLogc(lvl, first)
default:
// Build a format string so that it will be similar to Sprint
Global.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...)
}
}
// Utility for info log messages (see Debug() for parameter explanation)
// Wrapper for (*Logger).Info
func Info(arg0 interface{}, args ...interface{}) {
const (
lvl = INFO
)
switch first := arg0.(type) {
case string:
// Use the string as a format string
Global.intLogf(lvl, first, args...)
case func() string:
// Log the closure (no other arguments used)
Global.intLogc(lvl, first)
default:
// Build a format string so that it will be similar to Sprint
Global.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...)
}
}
// Utility for warn log messages (returns an error for easy function returns) (see Debug() for parameter explanation)
// These functions will execute a closure exactly once, to build the error message for the return
// Wrapper for (*Logger).Warn
func Warn(arg0 interface{}, args ...interface{}) error {
const (
lvl = WARNING
)
switch first := arg0.(type) {
case string:
// Use the string as a format string
Global.intLogf(lvl, first, args...)
return errors.New(fmt.Sprintf(first, args...))
case func() string:
// Log the closure (no other arguments used)
str := first()
Global.intLogf(lvl, "%s", str)
return errors.New(str)
default:
// Build a format string so that it will be similar to Sprint
Global.intLogf(lvl, fmt.Sprint(first)+strings.Repeat(" %v", len(args)), args...)
return errors.New(fmt.Sprint(first) + fmt.Sprintf(strings.Repeat(" %v", len(args)), args...))
}
return nil
}
// Utility for error log messages (returns an error for easy function returns) (see Debug() for parameter explanation)
// These functions will execute a closure exactly once, to build the error message for the return
// Wrapper for (*Logger).Error
func Error(arg0 interface{}, args ...interface{}) error {
const (
lvl = ERROR
)
switch first := arg0.(type) {
case string:
// Use the string as a format string
Global.intLogf(lvl, first, args...)
return errors.New(fmt.Sprintf(first, args...))
case func() string:
// Log the closure (no other arguments used)
str := first()
Global.intLogf(lvl, "%s", str)
return errors.New(str)
default:
// Build a format string so that it will be similar to Sprint
Global.intLogf(lvl, fmt.Sprint(first)+strings.Repeat(" %v", len(args)), args...)
return errors.New(fmt.Sprint(first) + fmt.Sprintf(strings.Repeat(" %v", len(args)), args...))
}
return nil
}
// Utility for critical log messages (returns an error for easy function returns) (see Debug() for parameter explanation)
// These functions will execute a closure exactly once, to build the error message for the return
// Wrapper for (*Logger).Critical
func Critical(arg0 interface{}, args ...interface{}) error {
const (
lvl = CRITICAL
)
switch first := arg0.(type) {
case string:
// Use the string as a format string
Global.intLogf(lvl, first, args...)
return errors.New(fmt.Sprintf(first, args...))
case func() string:
// Log the closure (no other arguments used)
str := first()
Global.intLogf(lvl, "%s", str)
return errors.New(str)
default:
// Build a format string so that it will be similar to Sprint
Global.intLogf(lvl, fmt.Sprint(first)+strings.Repeat(" %v", len(args)), args...)
return errors.New(fmt.Sprint(first) + fmt.Sprintf(strings.Repeat(" %v", len(args)), args...))
}
return nil
}

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
)

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