4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-06-27 18:09:26 +00:00

Compare commits

...

124 Commits

Author SHA1 Message Date
Wim
222cccf388 Release v1.8.0 2018-02-21 20:42:26 +01:00
Wim
bab308508e Fix the UseInsecureURL text (telegram). Closes #184 2018-02-21 13:30:38 +01:00
Wim
dedb83c867 Add ssh-chat to README 2018-02-21 01:42:43 +01:00
Wim
723a90cdd6 Exclude gofmt test from travis for now 2018-02-21 01:20:38 +01:00
Wim
67d2398fa8 Make matterclient work with prefixed log 2018-02-21 01:11:41 +01:00
Wim
5f3b6ec007 Disable echo banner and output (api) 2018-02-21 00:49:10 +01:00
Wim
55ab0c12f1 Update vendor labstack/echo 2018-02-21 00:48:10 +01:00
Wim
d1227b5fc9 Use prefixed-formatter for better logging 2018-02-21 00:20:25 +01:00
Wim
6ea368c383 Move Sirupsen => sirupsen 2018-02-20 23:41:09 +01:00
Wim
e92b6de09f Add more debug 2018-02-20 23:36:29 +01:00
Wim
e622587db4 Add label support in RemoteNickFormat 2018-02-20 18:57:46 +01:00
Wim
f2efc06d1f Give api access to whole config.Message (and events). Closes #374 2018-02-20 18:36:44 +01:00
Wim
a2b94452db Add more debug (telegram) 2018-02-20 17:51:23 +01:00
Wim
4c506f7cc3 Use MediaServerDownload instead of MediaServerUpload for avatars 2018-02-20 17:15:54 +01:00
Wim
7886f05e88 Download (and upload) avatar images from mattermost and telegram when mediaserver is configured. Closes #362
An extra avatarMap (cache) is created for mattermost and telegram.
If MediaServerUpload is configured, the avatar images of users are downloaded the first time a
user sends a message.
If this download succeeds a message with EVENT_AVATAR_DOWNLOAD is sent to the originating protocol.
This message also contains a SHA field (in msg.Extra["file"]), if this is not empty, the sha will
be added to the avatarMap. (so we now have a userid-sha cache)

Next time this user sends a message, the MediaServerUpload/sha/userid.png URL will be used as the
avatar field.
2018-02-20 01:15:25 +01:00
Wim
f58be0d1c1 Add SHA to FileInfo 2018-02-15 23:18:58 +01:00
Wim
1152394bc1 Update issue template 2018-02-15 22:35:29 +01:00
Wim
a082b5a590 Remove unused code 2018-02-15 00:07:25 +01:00
Wim
bae9484df2 Use discordgo ContentWithMoreMentionsReplace (discord) 2018-02-14 23:05:50 +01:00
Wim
6f78485878 Fix role replace 2018-02-14 23:05:16 +01:00
Wim
fd0fe3390b Update vendor bwmarrin/discordgo 2018-02-14 22:22:35 +01:00
Wim
2522158127 Add avator to fileinfo 2018-02-14 22:20:27 +01:00
Wim
8be107cecc Fix mattermost API change 2018-02-09 00:11:20 +01:00
Wim
5aab158c0b Update vendor (github.com/mattermost) 2018-02-09 00:11:04 +01:00
1d33e60e36 Truncate messages sent to IRC based on byte count (#368)
* Truncate messages sent to IRC based on byte count

* Avoid unnecessary string allocations
2018-02-08 23:28:33 +01:00
Wim
83c28cb857 Check for a valid WebhookURL (discord). Closes #367 2018-02-07 14:57:38 +01:00
Wim
df5bce27b0 Fix panic on nil messages (telegram). Closes #366 2018-02-07 14:28:48 +01:00
Wim
2b15739b48 Remove double close 2018-02-07 00:05:10 +01:00
Wim
3480c88e90 Do not close body on err. Closes #364 2018-02-07 00:04:02 +01:00
Wim
432cd0f99d Add more parsemode debug (telegram) 2018-02-04 17:55:20 +01:00
Wim
e8b3e9b22d Update readme 2018-02-04 16:07:37 +01:00
Wim
d4a47671ea Add markdown support (telegram). #355 2018-02-03 23:31:21 +01:00
Wim
0bcd1e62f3 Add channel_purpose to ShowTopicChange. Ignore (un)pinned_item (slack). #353 2018-02-03 01:15:57 +01:00
Wim
80822b7fff Send chat notification if media is too big to be re-uploaded to MediaServer. See #359 2018-02-03 01:11:11 +01:00
Wim
78f1011f52 Add support for file comments (slack). Closes #346 2018-02-02 23:16:10 +01:00
Wim
67f6257617 Add ShowTopicChange option. Allow/disable topic change messages (currently only from slack). Closes #353 2018-02-02 21:08:13 +01:00
Wim
169c614489 Download files and reupload to supported bridges (mattermost). Closes #357 2018-02-02 20:23:55 +01:00
da908c438a Add space between colon and URL for uploaded media (#360) 2018-02-01 17:46:10 +01:00
Wim
9c9c4bf1f9 Fix build 2018-02-01 01:01:25 +01:00
Wim
7764493298 Add comment to file upload from telegram. Show comments on all bridges. Closes #358 2018-02-01 00:41:09 +01:00
Wim
64a20ee61b Add URL to message in webhook if available (mattermost). See #356 2018-01-31 17:35:13 +01:00
Wim
62d1af8c37 Bump version 2018-01-29 12:41:35 +01:00
Wim
0f5274fdf6 Release v1.7.1 2018-01-29 12:35:35 +01:00
2e2187ebf4 Enable Long Polling for Telegram. Reduces bandwidth consumption. (#350)
Fixes #349.
2018-01-29 12:07:26 +01:00
Wim
762c3350f4 Bump version 2018-01-28 19:48:02 +01:00
Wim
e1a4d7f77e Update readme about REST api projects (matterlink,pycord) 2018-01-28 19:47:48 +01:00
Wim
a7a4554a85 Release v1.7.0 2018-01-28 19:36:02 +01:00
Wim
6bd808ce91 Lowercase irc channels in config. Closes #348 2018-01-28 19:15:13 +01:00
Wim
a5c143bc46 Allow xmpp to receive the extra messages when text is empty. #295 2018-01-27 16:32:38 +01:00
87c9cac756 Use cmosh/alpine-arm to build arm docker images (#347) 2018-01-27 13:49:13 +01:00
Wim
6a047f8722 Print only debug messages when specified (xmpp). Closes #345 2018-01-26 21:54:09 +01:00
Wim
6523494e83 Obey the Gateway value from the json (api). Closes #344 2018-01-21 12:21:55 +01:00
Wim
7c6ce8bb90 Fix xmpp badge, add twitch badge 2018-01-20 23:59:54 +01:00
Wim
dafbfe4021 Add twitch support (irc) to README 2018-01-20 23:38:58 +01:00
Wim
a4d5c94d9b Make edits/delete work for bridges that gets reused. Closes #342 2018-01-20 21:58:59 +01:00
Wim
7119e378a7 Add an extension to images without one (matrix). #331 2018-01-20 18:19:17 +01:00
Wim
e1dc3032c1 Ignore <subject> messages (xmpp). #272 2018-01-14 23:43:34 +01:00
Wim
5de03b8921 Update xmpp 2018-01-14 22:31:45 +01:00
Wim
7631d43c48 Change RemoteNickFormat replacement order. Closes #336 2018-01-14 16:55:32 +01:00
Wim
d0b2ee5c85 Add support for docker arm builds. #328 2018-01-10 00:04:24 +01:00
Wim
8830a5a1df Fix possible panics (matrix). Closes #333 2018-01-09 23:25:58 +01:00
Wim
ee87626a93 Update for 1.6.3 2018-01-09 00:13:46 +01:00
Wim
9f15d38c1c Use upstream again (slack) 2018-01-08 22:41:58 +01:00
Wim
4a96a977c0 Update vendor (slack) 2018-01-08 22:41:38 +01:00
9a95293bdf Convert received IRC channel names to lowercase. Fixes #329 (#330) 2018-01-06 22:55:03 +01:00
Wim
0b3a06d263 Log ConnectionErrorEvent (slack) 2018-01-03 14:06:28 +01:00
Wim
9a6249c4f5 Increase debug logging (slack) 2018-01-02 14:39:27 +01:00
Wim
50bd51e461 Use a better check to join channel (slack) 2018-01-02 14:31:44 +01:00
Wim
04f8013314 Bump version 2018-01-01 15:13:05 +01:00
Wim
a0aaf0057a Update for 1.6.2 2018-01-01 15:12:32 +01:00
Wim
8e78b3e6be Fix regression in mattermost bridge (mattermost). Closes #327 2018-01-01 14:20:16 +01:00
Wim
57a503818d Release v1.6.1 2017-12-26 19:22:50 +01:00
Wim
25d2ff3e9b Fix regression. Closes #323 2017-12-26 19:13:27 +01:00
Wim
31902d3e57 Add support for deleting messages from/to matrix (matrix). Closes #320 2017-12-25 00:55:39 +01:00
Wim
16f3fa6bae Vendor github.com/matterbridge/gomatrix 2017-12-25 00:54:39 +01:00
Wim
1f706673cf Bump version 2017-12-23 00:53:12 +01:00
Wim
fac5f69ad2 Release v1.6.0 2017-12-23 00:28:01 +01:00
Wim
97c944bb63 Add RejoinDelay option. Delay to rejoin after channel kick (irc). Closes #322 2017-12-23 00:11:30 +01:00
Wim
d0c4fe78ee Allow specifying maximum download size of media using MediaDownloadSize (slack,telegram,matrix) 2017-12-19 23:44:13 +01:00
Wim
265457b451 Refactor and add MediaDownloadSize to General 2017-12-19 23:15:03 +01:00
Wim
4a4a29c9f6 Fix panic (matrix). Closes #316 2017-12-11 12:25:28 +01:00
Wim
0a91b9e1c9 Fix incorrect forward from text line (telegram) 2017-12-11 12:15:26 +01:00
Wim
f56163295c Remove unreachable code (api) 2017-12-10 15:20:17 +01:00
Wim
d1c87c068b Also use HTML in edited messages (telegram). Closes #315 2017-12-10 15:16:17 +01:00
Wim
fa20761110 Add support for Audio/Voice files (telegram). Closes #314 2017-12-10 15:08:23 +01:00
Wim
e4a0e0a0e9 Add support for forwarded messages. Closes #313 2017-12-10 14:52:29 +01:00
Wim
d30ae19e2a Add (simple, one listener) long-polling support (api). Closes #307 2017-12-07 23:48:44 +01:00
Wim
5c919e6bff Update vendor labstack/echo 2017-12-07 23:00:56 +01:00
Wim
434393d1c3 Update README 2017-12-07 22:30:17 +01:00
Wim
af9aa5d7cb Update changelog 2017-12-07 22:27:17 +01:00
Wim
05eb75442a Split on UTF-8 for MessageSplit (irc). Closes #308 2017-12-07 22:21:54 +01:00
Wim
3496ed0c7e Fix irc ACTION regression (irc). Closes #306 2017-12-07 22:07:45 +01:00
Wim
1b89604c7a Bump version 2017-12-03 01:29:54 +01:00
Wim
67a9d133e9 Add quick & dirty sshchat support (https://github.com/shazow/ssh-chat) 2017-12-03 01:29:25 +01:00
Wim
ed9118b346 Add sshchat dependencies in vendor 2017-12-03 01:24:05 +01:00
Wim
59e55cfbd5 Release v1.5.0 2017-12-03 00:01:05 +01:00
Wim
788d3b32ac Update vendor lrstanley/girc and readme 2017-12-02 23:58:02 +01:00
Wim
1d414cf2fd Allow ^ in nick (irc). Closes #305 2017-11-30 00:28:17 +01:00
Wim
cc3c168162 Update vendor lrstanley/girc 2017-11-30 00:27:31 +01:00
Wim
1ee6837f0e Update changelog 2017-11-24 23:56:22 +01:00
Wim
27dcea7c5b Update documentation about ReplaceMessages and ReplaceNicks 2017-11-24 23:45:00 +01:00
Wim
dcda7f7b8c Add documentation about MediaServerUpload and MediaServerDownload 2017-11-24 23:35:25 +01:00
Wim
e0cbb69a4f Add MessageSplit option to split messages on MessageLength (irc). Closes #281 2017-11-24 23:29:00 +01:00
Wim
7ec95f786d Use mediaserver urls for irc,gitter and xmpp 2017-11-24 22:55:24 +01:00
Wim
1efe40add5 Add initial support for an external mediaserver. #278
Add 2 extra options `MediaServerUpload` and `MediaServerDownload`, where
the URL for upload and download can be specified.

See https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%5Badvanced%5D
for an example with caddy
2017-11-24 22:36:19 +01:00
Wim
cbd73ee313 Add support for uploaded images/video/files (matrix) 2017-11-22 00:28:40 +01:00
Wim
34227a7a39 Add support for uploading images/video (matrix). Closes #302 2017-11-21 23:50:27 +01:00
Wim
71cb9b2d1d Update vendor github.com/matrix-org/gomatrix 2017-11-21 23:48:39 +01:00
Wim
cd4c9b194f Add support for ReplaceNicks using regexp to replace nicks. Closes #269 2017-11-20 23:27:27 +01:00
Wim
98762a0235 Add webp extension to stickers if necessary (telegram) 2017-11-20 22:12:51 +01:00
Wim
2fd1fd9573 Break when re-login fails (mattermost) 2017-11-16 20:19:52 +01:00
Wim
aff3964078 Add support for ReplaceMessages using regexp to replace messages. #269 2017-11-15 23:33:00 +01:00
Wim
2778580397 Bump version 2017-11-13 20:13:32 +01:00
Wim
962062fe44 Release v1.4.1 2017-11-13 20:10:04 +01:00
Wim
0578b21270 Fix message sending (slack) 2017-11-13 19:50:18 +01:00
Wim
36a800c3f5 Add support for comments from slack file uploads (slack) 2017-11-13 00:20:31 +01:00
Wim
6d21f84187 Add extension to sticker/video/photo (telegram) 2017-11-12 22:04:35 +01:00
Wim
f1e9833310 Do not ignore empty messages with files for bridges that support it 2017-11-12 18:34:16 +01:00
Wim
46f5acc4f9 Add the download actually to the message (telegram) 2017-11-12 18:09:38 +01:00
Wim
95d4dcaeb3 Add more debug info (telegram) 2017-11-12 17:49:10 +01:00
Wim
64c542e614 Add more debug info (telegram) 2017-11-12 17:46:44 +01:00
Wim
13d081ea80 Fix document bug (telegram) 2017-11-12 17:15:53 +01:00
Wim
c0f9d86287 Fix telegram photo/document input handling (telegram) 2017-11-12 11:46:32 +01:00
Wim
bcdecdaa73 Fix strict user handling of girc (irc). Closes #298 2017-11-11 23:16:58 +01:00
733 changed files with 116862 additions and 33150 deletions

View File

@ -1,22 +1,36 @@
If you have a configuration problem, please first try to create a basic configuration following the instructions on [the wiki](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) before filing an issue. <!-- This is a bug report template. By following the instructions below and
filling out the sections with your information, you will help the us to get all
the necessary data to fix your issue.
Please answer the following questions. You can also preview your report before submitting it.
### Which version of matterbridge are you using? Text between <!-- and --> marks will be invisible in the report.
run ```matterbridge -version``` -->
### If you're having problems with mattermost please specify mattermost version. <!-- If you have a configuration problem, please first try to create a basic configuration following the instructions on [the wiki](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) before filing an issue. -->
### Environment
<!-- run `matterbridge -version` -->
<!-- If you're having problems with mattermost also specify the mattermost version. -->
Version:
<!-- What operating system are you using ? (be as specific as possible) -->
Operating system:
<!-- If you compiled matterbridge yourself:
* Specify the output of `go version`
* Specify the output of `git rev-parse HEAD` -->
### Please describe the expected behavior. ### Please describe the expected behavior.
### Please describe the actual behavior. ### Please describe the actual behavior.
#### Use logs from running ```matterbridge -debug``` if possible. <!-- Use logs from running `matterbridge -debug` if possible. -->
### Any steps to reproduce the behavior? ### Any steps to reproduce the behavior?
### Please add your configuration file ### Please add your configuration file
#### (be sure to exclude or anonymize private data (tokens/passwords)) <!-- (be sure to exclude or anonymize private data (tokens/passwords)) -->

View File

@ -34,7 +34,7 @@ before_script:
# flunk the build and immediately stop. It's sorta like having # flunk the build and immediately stop. It's sorta like having
# set -e enabled in bash. # set -e enabled in bash.
script: script:
- test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt #- test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt
- go test -v -race $PKGS # Run all the tests with the race detector enabled - go test -v -race $PKGS # Run all the tests with the race detector enabled
- go vet $PKGS # go vet is the official Go static analyzer - go vet $PKGS # go vet is the official Go static analyzer
- megacheck $PKGS # "go vet on steroids" + linter - megacheck $PKGS # "go vet on steroids" + linter

View File

@ -1,17 +1,18 @@
# matterbridge # matterbridge
Click on one of the badges below to join the chat Click on one of the badges below to join the chat
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?colorB=42f4242)](https://gitter.im/42wim/matterbridge) [![Join the IRC chat at https://webchat.freenode.net/?channels=matterbridgechat](https://img.shields.io/badge/IRC-matterbridgechat-green.svg?colorB=42f4242)](https://webchat.freenode.net/?channels=matterbridgechat) [![Discord](https://img.shields.io/badge/discord-matterbridge-green.svg?colorB=42f4242)](https://discord.gg/AkKPtrQ) [![Matrix](https://img.shields.io/badge/matrix-matterbridge-green.svg?colorB=42f4242)](https://riot.im/app/#/room/#matterbridge:matrix.org) [![Slack](https://img.shields.io/badge/slack-matterbridgechat-green.svg?colorB=42f4242)](https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA) [![Mattermost](https://img.shields.io/badge/mattermost-matterbridge-green.svg?colorB=42f4242)](https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e) ![Xmpp](https://img.shields.io/badge/xmpp-matterbridge@muc.im.koderoot.net-green.svg?colorB=42f4242) [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?colorB=42f4242)](https://gitter.im/42wim/matterbridge) [![Join the IRC chat at https://webchat.freenode.net/?channels=matterbridgechat](https://img.shields.io/badge/IRC-matterbridgechat-green.svg?colorB=42f4242)](https://webchat.freenode.net/?channels=matterbridgechat) [![Discord](https://img.shields.io/badge/discord-matterbridge-green.svg?colorB=42f4242)](https://discord.gg/AkKPtrQ) [![Matrix](https://img.shields.io/badge/matrix-matterbridge-green.svg?colorB=42f4242)](https://riot.im/app/#/room/#matterbridge:matrix.org) [![Slack](https://img.shields.io/badge/slack-matterbridgechat-green.svg?colorB=42f4242)](https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA) [![Mattermost](https://img.shields.io/badge/mattermost-matterbridge-green.svg?colorB=42f4242)](https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e) [![Xmpp](https://img.shields.io/badge/xmpp-matterbridge@conference.jabber.de-green.svg?colorB=42f4242)](https://inverse.chat) [![Twitch](https://img.shields.io/badge/twitch-matterbridge-green.svg?colorB=42f4242)](https://www.twitch.tv/matterbridge)
[![Download stable](https://img.shields.io/github/release/42wim/matterbridge.svg?label=download%20stable)](https://github.com/42wim/matterbridge/releases/latest) [![Download dev](https://img.shields.io/bintray/v/42wim/nightly/Matterbridge.svg?label=download%20dev&colorB=007ec6)](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion) [![Download stable](https://img.shields.io/github/release/42wim/matterbridge.svg?label=download%20stable)](https://github.com/42wim/matterbridge/releases/latest) [![Download dev](https://img.shields.io/bintray/v/42wim/nightly/Matterbridge.svg?label=download%20dev&colorB=007ec6)](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion)
![matterbridge.gif](https://s15.postimg.org/qpjhp6y3f/matterbridge.gif) ![matterbridge.gif](https://s15.postimg.org/qpjhp6y3f/matterbridge.gif)
Simple bridge between Mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat(via xmpp), Matrix and Steam. Simple bridge between Mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat(via xmpp), Matrix, Steam and ssh-chat
Has a REST API. Has a REST API.
Minecraft server chat support via [MatterLink](https://github.com/elytra/MatterLink)
# Table of Contents # Table of Contents
* [Features](#features) * [Features](https://github.com/42wim/matterbridge/wiki/Features)
* [Requirements](#requirements) * [Requirements](#requirements)
* [Screenshots](https://github.com/42wim/matterbridge/wiki/) * [Screenshots](https://github.com/42wim/matterbridge/wiki/)
* [Installing](#installing) * [Installing](#installing)
@ -27,13 +28,21 @@ Has a REST API.
* [Thanks](#thanks) * [Thanks](#thanks)
# Features # Features
* Relays public channel messages between multiple mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat (via xmpp), Matrix and Steam. * [Support bridging between any protocols](https://github.com/42wim/matterbridge/wiki/Features#support-bridging-between-any-protocols)
Pick and mix. * [Support multiple gateways(bridges) for your protocols](https://github.com/42wim/matterbridge/wiki/Features#support-multiple-gatewaysbridges-for-your-protocols)
* Support private groups on your mattermost/slack. * [Message edits and deletes](https://github.com/42wim/matterbridge/wiki/Features#message-edits-and-deletes)
* Allow for bridging the same bridges, which means you can eg bridge between multiple mattermosts. * [Attachment / files handling](https://github.com/42wim/matterbridge/wiki/Features#attachment--files-handling)
* The bridge is now a gateway which has support multiple in and out bridges. (and supports multiple gateways). * [Username and avatar spoofing](https://github.com/42wim/matterbridge/wiki/Features#username-and-avatar-spoofing)
* Edits and delete messages across bridges that support it (mattermost,slack,discord,gitter,telegram) * [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups)
* REST API to read/post messages to bridges (WIP). * [API](https://github.com/42wim/matterbridge/wiki/Features#api)
## API
The API is very basic at the moment and rather undocumented.
Used by at least 2 projects. Feel free to make a PR to add your project to this list.
* [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Server chat)
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
# Requirements # Requirements
Accounts to one of the supported bridges Accounts to one of the supported bridges
@ -48,13 +57,15 @@ Accounts to one of the supported bridges
* [Rocket.chat](https://rocket.chat) * [Rocket.chat](https://rocket.chat)
* [Matrix](https://matrix.org) * [Matrix](https://matrix.org)
* [Steam](https://store.steampowered.com/) * [Steam](https://store.steampowered.com/)
* [Twitch](https://twitch.tv)
* [Ssh-chat](https://github.com/shazow/ssh-chat)
# Screenshots # Screenshots
See https://github.com/42wim/matterbridge/wiki See https://github.com/42wim/matterbridge/wiki
# Installing # Installing
## Binaries ## Binaries
* Latest stable release [v1.4.0](https://github.com/42wim/matterbridge/releases/latest) * Latest stable release [v1.8.0](https://github.com/42wim/matterbridge/releases/latest)
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/) * Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
## Building ## Building
@ -175,7 +186,7 @@ Matterbridge wouldn't exist without these libraries:
* echo - https://github.com/labstack/echo * echo - https://github.com/labstack/echo
* gitter - https://github.com/sromku/go-gitter * gitter - https://github.com/sromku/go-gitter
* gops - https://github.com/google/gops * gops - https://github.com/google/gops
* irc - https://github.com/thoj/go-ircevent * irc - https://github.com/lrstanley/girc
* mattermost - https://github.com/mattermost/platform * mattermost - https://github.com/mattermost/platform
* matrix - https://github.com/matrix-org/gomatrix * matrix - https://github.com/matrix-org/gomatrix
* slack - https://github.com/nlopes/slack * slack - https://github.com/nlopes/slack

View File

@ -1,21 +1,21 @@
package api package api
import ( import (
"encoding/json"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/labstack/echo" "github.com/labstack/echo"
"github.com/labstack/echo/middleware" "github.com/labstack/echo/middleware"
log "github.com/sirupsen/logrus"
"github.com/zfjagann/golang-ring" "github.com/zfjagann/golang-ring"
"net/http" "net/http"
"sync" "sync"
"time"
) )
type Api struct { type Api struct {
Config *config.Protocol
Remote chan config.Message
Account string
Messages ring.Ring Messages ring.Ring
sync.RWMutex sync.RWMutex
*config.BridgeConfig
} }
type ApiMessage struct { type ApiMessage struct {
@ -30,26 +30,30 @@ var flog *log.Entry
var protocol = "api" var protocol = "api"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Api { func New(cfg *config.BridgeConfig) *Api {
b := &Api{} b := &Api{BridgeConfig: cfg}
e := echo.New() e := echo.New()
e.HideBanner = true
e.HidePort = true
b.Messages = ring.Ring{} b.Messages = ring.Ring{}
b.Messages.SetCapacity(cfg.Buffer) b.Messages.SetCapacity(b.Config.Buffer)
b.Config = &cfg
b.Account = account
b.Remote = c
if b.Config.Token != "" { if b.Config.Token != "" {
e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
return key == b.Config.Token, nil return key == b.Config.Token, nil
})) }))
} }
e.GET("/api/messages", b.handleMessages) e.GET("/api/messages", b.handleMessages)
e.GET("/api/stream", b.handleStream)
e.POST("/api/message", b.handlePostMessage) e.POST("/api/message", b.handlePostMessage)
go func() { go func() {
flog.Fatal(e.Start(cfg.BindAddress)) if b.Config.BindAddress == "" {
flog.Fatalf("No BindAddress configured.")
}
flog.Infof("Listening on %s", b.Config.BindAddress)
flog.Fatal(e.Start(b.Config.BindAddress))
}() }()
return b return b
} }
@ -78,21 +82,18 @@ func (b *Api) Send(msg config.Message) (string, error) {
} }
func (b *Api) handlePostMessage(c echo.Context) error { func (b *Api) handlePostMessage(c echo.Context) error {
message := &ApiMessage{} message := config.Message{}
if err := c.Bind(message); err != nil { if err := c.Bind(&message); err != nil {
return err return err
} }
// these values are fixed
message.Channel = "api"
message.Protocol = "api"
message.Account = b.Account
message.ID = ""
message.Timestamp = time.Now()
flog.Debugf("Sending message from %s on %s to gateway", message.Username, "api") flog.Debugf("Sending message from %s on %s to gateway", message.Username, "api")
b.Remote <- config.Message{ b.Remote <- message
Text: message.Text,
Username: message.Username,
UserID: message.UserID,
Channel: "api",
Avatar: message.Avatar,
Account: b.Account,
Gateway: message.Gateway,
Protocol: "api",
}
return c.JSON(http.StatusOK, message) return c.JSON(http.StatusOK, message)
} }
@ -103,3 +104,24 @@ func (b *Api) handleMessages(c echo.Context) error {
b.Messages = ring.Ring{} b.Messages = ring.Ring{}
return nil return nil
} }
func (b *Api) handleStream(c echo.Context) error {
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
c.Response().WriteHeader(http.StatusOK)
closeNotifier := c.Response().CloseNotify()
for {
select {
case <-closeNotifier:
return nil
default:
msg := b.Messages.Dequeue()
if msg != nil {
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
return err
}
c.Response().Flush()
}
time.Sleep(200 * time.Millisecond)
}
}
}

View File

@ -10,10 +10,11 @@ import (
"github.com/42wim/matterbridge/bridge/mattermost" "github.com/42wim/matterbridge/bridge/mattermost"
"github.com/42wim/matterbridge/bridge/rocketchat" "github.com/42wim/matterbridge/bridge/rocketchat"
"github.com/42wim/matterbridge/bridge/slack" "github.com/42wim/matterbridge/bridge/slack"
"github.com/42wim/matterbridge/bridge/sshchat"
"github.com/42wim/matterbridge/bridge/steam" "github.com/42wim/matterbridge/bridge/steam"
"github.com/42wim/matterbridge/bridge/telegram" "github.com/42wim/matterbridge/bridge/telegram"
"github.com/42wim/matterbridge/bridge/xmpp" "github.com/42wim/matterbridge/bridge/xmpp"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
"strings" "strings"
) )
@ -35,6 +36,12 @@ type Bridge struct {
Joined map[string]bool Joined map[string]bool
} }
var flog *log.Entry
func init() {
flog = log.WithFields(log.Fields{"prefix": "bridge"})
}
func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Bridge { func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Bridge {
b := new(Bridge) b := new(Bridge)
b.Channels = make(map[string]config.ChannelInfo) b.Channels = make(map[string]config.ChannelInfo)
@ -45,44 +52,49 @@ func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Brid
b.Protocol = protocol b.Protocol = protocol
b.Account = bridge.Account b.Account = bridge.Account
b.Joined = make(map[string]bool) b.Joined = make(map[string]bool)
bridgeConfig := &config.BridgeConfig{General: &cfg.General, Account: bridge.Account, Remote: c}
// override config from environment // override config from environment
config.OverrideCfgFromEnv(cfg, protocol, name) config.OverrideCfgFromEnv(cfg, protocol, name)
switch protocol { switch protocol {
case "mattermost": case "mattermost":
b.Config = cfg.Mattermost[name] bridgeConfig.Config = cfg.Mattermost[name]
b.Bridger = bmattermost.New(cfg.Mattermost[name], bridge.Account, c) b.Bridger = bmattermost.New(bridgeConfig)
case "irc": case "irc":
b.Config = cfg.IRC[name] bridgeConfig.Config = cfg.IRC[name]
b.Bridger = birc.New(cfg.IRC[name], bridge.Account, c) b.Bridger = birc.New(bridgeConfig)
case "gitter": case "gitter":
b.Config = cfg.Gitter[name] bridgeConfig.Config = cfg.Gitter[name]
b.Bridger = bgitter.New(cfg.Gitter[name], bridge.Account, c) b.Bridger = bgitter.New(bridgeConfig)
case "slack": case "slack":
b.Config = cfg.Slack[name] bridgeConfig.Config = cfg.Slack[name]
b.Bridger = bslack.New(cfg.Slack[name], bridge.Account, c) b.Bridger = bslack.New(bridgeConfig)
case "xmpp": case "xmpp":
b.Config = cfg.Xmpp[name] bridgeConfig.Config = cfg.Xmpp[name]
b.Bridger = bxmpp.New(cfg.Xmpp[name], bridge.Account, c) b.Bridger = bxmpp.New(bridgeConfig)
case "discord": case "discord":
b.Config = cfg.Discord[name] bridgeConfig.Config = cfg.Discord[name]
b.Bridger = bdiscord.New(cfg.Discord[name], bridge.Account, c) b.Bridger = bdiscord.New(bridgeConfig)
case "telegram": case "telegram":
b.Config = cfg.Telegram[name] bridgeConfig.Config = cfg.Telegram[name]
b.Bridger = btelegram.New(cfg.Telegram[name], bridge.Account, c) b.Bridger = btelegram.New(bridgeConfig)
case "rocketchat": case "rocketchat":
b.Config = cfg.Rocketchat[name] bridgeConfig.Config = cfg.Rocketchat[name]
b.Bridger = brocketchat.New(cfg.Rocketchat[name], bridge.Account, c) b.Bridger = brocketchat.New(bridgeConfig)
case "matrix": case "matrix":
b.Config = cfg.Matrix[name] bridgeConfig.Config = cfg.Matrix[name]
b.Bridger = bmatrix.New(cfg.Matrix[name], bridge.Account, c) b.Bridger = bmatrix.New(bridgeConfig)
case "steam": case "steam":
b.Config = cfg.Steam[name] bridgeConfig.Config = cfg.Steam[name]
b.Bridger = bsteam.New(cfg.Steam[name], bridge.Account, c) b.Bridger = bsteam.New(bridgeConfig)
case "sshchat":
bridgeConfig.Config = cfg.Sshchat[name]
b.Bridger = bsshchat.New(bridgeConfig)
case "api": case "api":
b.Config = cfg.Api[name] bridgeConfig.Config = cfg.Api[name]
b.Bridger = api.New(cfg.Api[name], bridge.Account, c) b.Bridger = api.New(bridgeConfig)
} }
b.Config = bridgeConfig.Config
return b return b
} }
@ -94,7 +106,7 @@ func (b *Bridge) JoinChannels() error {
func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error { func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error {
for ID, channel := range channels { for ID, channel := range channels {
if !exists[ID] { if !exists[ID] {
log.Infof("%s: joining %s (%s)", b.Account, channel.Name, ID) flog.Infof("%s: joining %s (ID: %s)", b.Account, channel.Name, ID)
err := b.JoinChannel(channel) err := b.JoinChannel(channel)
if err != nil { if err != nil {
return err return err

View File

@ -10,11 +10,14 @@ import (
) )
const ( const (
EVENT_JOIN_LEAVE = "join_leave" EVENT_JOIN_LEAVE = "join_leave"
EVENT_FAILURE = "failure" EVENT_TOPIC_CHANGE = "topic_change"
EVENT_REJOIN_CHANNELS = "rejoin_channels" EVENT_FAILURE = "failure"
EVENT_USER_ACTION = "user_action" EVENT_FILE_FAILURE_SIZE = "file_failure_size"
EVENT_MSG_DELETE = "msg_delete" EVENT_AVATAR_DOWNLOAD = "avatar_download"
EVENT_REJOIN_CHANNELS = "rejoin_channels"
EVENT_USER_ACTION = "user_action"
EVENT_MSG_DELETE = "msg_delete"
) )
type Message struct { type Message struct {
@ -33,8 +36,13 @@ type Message struct {
} }
type FileInfo struct { type FileInfo struct {
Name string Name string
Data *[]byte Data *[]byte
Comment string
URL string
Size int64
Avatar bool
SHA string
} }
type ChannelInfo struct { type ChannelInfo struct {
@ -51,48 +59,58 @@ type Protocol struct {
BindAddress string // mattermost, slack // DEPRECATED BindAddress string // mattermost, slack // DEPRECATED
Buffer int // api Buffer int // api
Charset string // irc Charset string // irc
Debug bool // general
EditSuffix string // mattermost, slack, discord, telegram, gitter EditSuffix string // mattermost, slack, discord, telegram, gitter
EditDisable bool // mattermost, slack, discord, telegram, gitter EditDisable bool // mattermost, slack, discord, telegram, gitter
IconURL string // mattermost, slack IconURL string // mattermost, slack
IgnoreNicks string // all protocols IgnoreNicks string // all protocols
IgnoreMessages string // all protocols IgnoreMessages string // all protocols
Jid string // xmpp Jid string // xmpp
Label string // all protocols
Login string // mattermost, matrix Login string // mattermost, matrix
Muc string // xmpp MediaDownloadSize int // all protocols
Name string // all protocols MediaServerDownload string
Nick string // all protocols MediaServerUpload string
NickFormatter string // mattermost, slack MessageDelay int // IRC, time in millisecond to wait between messages
NickServNick string // IRC MessageFormat string // telegram
NickServUsername string // IRC MessageLength int // IRC, max length of a message allowed
NickServPassword string // IRC MessageQueue int // IRC, size of message queue for flood control
NicksPerRow int // mattermost, slack MessageSplit bool // IRC, split long messages with newlines on MessageLength instead of clipping
NoHomeServerSuffix bool // matrix Muc string // xmpp
NoTLS bool // mattermost Name string // all protocols
Password string // IRC,mattermost,XMPP,matrix Nick string // all protocols
PrefixMessagesWithNick bool // mattemost, slack NickFormatter string // mattermost, slack
Protocol string //all protocols NickServNick string // IRC
MessageQueue int // IRC, size of message queue for flood control NickServUsername string // IRC
MessageDelay int // IRC, time in millisecond to wait between messages NickServPassword string // IRC
MessageLength int // IRC, max length of a message allowed NicksPerRow int // mattermost, slack
MessageFormat string // telegram NoHomeServerSuffix bool // matrix
RemoteNickFormat string // all protocols NoTLS bool // mattermost
Server string // IRC,mattermost,XMPP,discord Password string // IRC,mattermost,XMPP,matrix
ShowJoinPart bool // all protocols PrefixMessagesWithNick bool // mattemost, slack
ShowEmbeds bool // discord Protocol string // all protocols
SkipTLSVerify bool // IRC, mattermost RejoinDelay int // IRC
StripNick bool // all protocols ReplaceMessages [][]string // all protocols
Team string // mattermost ReplaceNicks [][]string // all protocols
Token string // gitter, slack, discord, api RemoteNickFormat string // all protocols
URL string // mattermost, slack // DEPRECATED Server string // IRC,mattermost,XMPP,discord
UseAPI bool // mattermost, slack ShowJoinPart bool // all protocols
UseSASL bool // IRC ShowTopicChange bool // slack
UseTLS bool // IRC ShowEmbeds bool // discord
UseFirstName bool // telegram SkipTLSVerify bool // IRC, mattermost
UseUserName bool // discord StripNick bool // all protocols
UseInsecureURL bool // telegram Team string // mattermost
WebhookBindAddress string // mattermost, slack Token string // gitter, slack, discord, api
WebhookURL string // mattermost, slack URL string // mattermost, slack // DEPRECATED
WebhookUse string // mattermost, slack, discord UseAPI bool // mattermost, slack
UseSASL bool // IRC
UseTLS bool // IRC
UseFirstName bool // telegram
UseUserName bool // discord
UseInsecureURL bool // telegram
WebhookBindAddress string // mattermost, slack
WebhookURL string // mattermost, slack
WebhookUse string // mattermost, slack, discord
} }
type ChannelOptions struct { type ChannelOptions struct {
@ -134,11 +152,19 @@ type Config struct {
Discord map[string]Protocol Discord map[string]Protocol
Telegram map[string]Protocol Telegram map[string]Protocol
Rocketchat map[string]Protocol Rocketchat map[string]Protocol
Sshchat map[string]Protocol
General Protocol General Protocol
Gateway []Gateway Gateway []Gateway
SameChannelGateway []SameChannelGateway SameChannelGateway []SameChannelGateway
} }
type BridgeConfig struct {
Config Protocol
General *Protocol
Account string
Remote chan Message
}
func NewConfig(cfgfile string) *Config { func NewConfig(cfgfile string) *Config {
var cfg Config var cfg Config
if _, err := toml.DecodeFile(cfgfile, &cfg); err != nil { if _, err := toml.DecodeFile(cfgfile, &cfg); err != nil {
@ -166,6 +192,9 @@ func NewConfig(cfgfile string) *Config {
if fail { if fail {
log.Fatalf("Fix your config. Please see changelog for more information") log.Fatalf("Fix your config. Please see changelog for more information")
} }
if cfg.General.MediaDownloadSize == 0 {
cfg.General.MediaDownloadSize = 1000000
}
return &cfg return &cfg
} }

View File

@ -3,8 +3,9 @@ package bdiscord
import ( import (
"bytes" "bytes"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" "github.com/42wim/matterbridge/bridge/helper"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
log "github.com/sirupsen/logrus"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
@ -12,9 +13,6 @@ import (
type bdiscord struct { type bdiscord struct {
c *discordgo.Session c *discordgo.Session
Config *config.Protocol
Remote chan config.Message
Account string
Channels []*discordgo.Channel Channels []*discordgo.Channel
Nick string Nick string
UseChannelID bool UseChannelID bool
@ -24,20 +22,18 @@ type bdiscord struct {
webhookToken string webhookToken string
channelInfoMap map[string]*config.ChannelInfo channelInfoMap map[string]*config.ChannelInfo
sync.RWMutex sync.RWMutex
*config.BridgeConfig
} }
var flog *log.Entry var flog *log.Entry
var protocol = "discord" var protocol = "discord"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *bdiscord { func New(cfg *config.BridgeConfig) *bdiscord {
b := &bdiscord{} b := &bdiscord{BridgeConfig: cfg}
b.Config = &cfg
b.Remote = c
b.Account = account
b.userMemberMap = make(map[string]*discordgo.Member) b.userMemberMap = make(map[string]*discordgo.Member)
b.channelInfoMap = make(map[string]*config.ChannelInfo) b.channelInfoMap = make(map[string]*config.ChannelInfo)
if b.Config.WebhookURL != "" { if b.Config.WebhookURL != "" {
@ -144,6 +140,9 @@ func (b *bdiscord) Send(msg config.Message) (string, error) {
} }
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text)
}
// check if we have files to upload (from slack, telegram or mattermost) // check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
var err error var err error
@ -151,11 +150,12 @@ func (b *bdiscord) Send(msg config.Message) (string, error) {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)
files := []*discordgo.File{} files := []*discordgo.File{}
files = append(files, &discordgo.File{fi.Name, "", bytes.NewReader(*fi.Data)}) files = append(files, &discordgo.File{fi.Name, "", bytes.NewReader(*fi.Data)})
_, err = b.c.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{Content: msg.Text, Files: files}) _, err = b.c.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{Content: msg.Username + fi.Comment, Files: files})
if err != nil { if err != nil {
flog.Errorf("file upload failed: %#v", err) flog.Errorf("file upload failed: %#v", err)
} }
} }
return "", nil
} }
} }
@ -202,6 +202,7 @@ func (b *bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdat
} }
func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
var err error
// not relay our own messages // not relay our own messages
if m.Author.Username == b.Nick { if m.Author.Username == b.Nick {
return return
@ -220,12 +221,13 @@ func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
var text string var text string
if m.Content != "" { if m.Content != "" {
flog.Debugf("Receiving message %#v", m.Message) flog.Debugf("Receiving message %#v", m.Message)
if len(m.MentionRoles) > 0 {
m.Message.Content = b.replaceRoleMentions(m.Message.Content)
}
m.Message.Content = b.stripCustomoji(m.Message.Content) m.Message.Content = b.stripCustomoji(m.Message.Content)
m.Message.Content = b.replaceChannelMentions(m.Message.Content) m.Message.Content = b.replaceChannelMentions(m.Message.Content)
text = m.ContentWithMentionsReplaced() text, err = m.ContentWithMoreMentionsReplaced(b.c)
if err != nil {
flog.Errorf("ContentWithMoreMentionsReplaced failed: %s", err)
text = m.ContentWithMentionsReplaced()
}
} }
rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg",
@ -322,18 +324,6 @@ func (b *bdiscord) getChannelName(id string) string {
return "" return ""
} }
func (b *bdiscord) replaceRoleMentions(text string) string {
roles, err := b.c.GuildRoles(b.guildID)
if err != nil {
flog.Debugf("%#v", string(err.(*discordgo.RESTError).ResponseBody))
return text
}
for _, role := range roles {
text = strings.Replace(text, "<@&"+role.ID+">", "@"+role.Name, -1)
}
return text
}
func (b *bdiscord) replaceChannelMentions(text string) string { func (b *bdiscord) replaceChannelMentions(text string) string {
var err error var err error
re := regexp.MustCompile("<#[0-9]+>") re := regexp.MustCompile("<#[0-9]+>")
@ -369,6 +359,9 @@ func (b *bdiscord) stripCustomoji(text string) string {
// splitURL splits a webhookURL and returns the id and token // splitURL splits a webhookURL and returns the id and token
func (b *bdiscord) splitURL(url string) (string, string) { func (b *bdiscord) splitURL(url string) (string, string) {
webhookURLSplit := strings.Split(url, "/") webhookURLSplit := strings.Split(url, "/")
if len(webhookURLSplit) != 7 {
log.Fatalf("%s is no correct discord WebhookURL", url)
}
return webhookURLSplit[len(webhookURLSplit)-2], webhookURLSplit[len(webhookURLSplit)-1] return webhookURLSplit[len(webhookURLSplit)-2], webhookURLSplit[len(webhookURLSplit)-1]
} }

View File

@ -4,33 +4,28 @@ import (
"fmt" "fmt"
"github.com/42wim/go-gitter" "github.com/42wim/go-gitter"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" "github.com/42wim/matterbridge/bridge/helper"
log "github.com/sirupsen/logrus"
"strings" "strings"
) )
type Bgitter struct { type Bgitter struct {
c *gitter.Gitter c *gitter.Gitter
Config *config.Protocol User *gitter.User
Remote chan config.Message Users []gitter.User
Account string Rooms []gitter.Room
User *gitter.User *config.BridgeConfig
Users []gitter.User
Rooms []gitter.Room
} }
var flog *log.Entry var flog *log.Entry
var protocol = "gitter" var protocol = "gitter"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Bgitter { func New(cfg *config.BridgeConfig) *Bgitter {
b := &Bgitter{} return &Bgitter{BridgeConfig: cfg}
b.Config = &cfg
b.Remote = c
b.Account = account
return b
} }
func (b *Bgitter) Connect() error { func (b *Bgitter) Connect() error {
@ -125,6 +120,29 @@ func (b *Bgitter) Send(msg config.Message) (string, error) {
} }
return "", nil return "", nil
} }
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.c.SendMessage(roomID, rmsg.Username+rmsg.Text)
}
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" {
msg.Text = fi.URL
}
_, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
if err != nil {
return "", err
}
}
return "", nil
}
}
resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text) resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -2,6 +2,8 @@ package helper
import ( import (
"bytes" "bytes"
"fmt"
"github.com/42wim/matterbridge/bridge/config"
"io" "io"
"net/http" "net/http"
"time" "time"
@ -18,11 +20,44 @@ func DownloadFile(url string) (*[]byte, error) {
} }
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
resp.Body.Close()
return nil, err return nil, err
} }
defer resp.Body.Close()
io.Copy(&buf, resp.Body) io.Copy(&buf, resp.Body)
data := buf.Bytes() data := buf.Bytes()
resp.Body.Close()
return &data, nil return &data, nil
} }
func SplitStringLength(input string, length int) string {
a := []rune(input)
str := ""
for i, r := range a {
str = str + string(r)
if i > 0 && (i+1)%length == 0 {
str += "\n"
}
}
return str
}
// handle all the stuff we put into extra
func HandleExtra(msg *config.Message, general *config.Protocol) []config.Message {
extra := msg.Extra
rmsg := []config.Message{}
if len(extra[config.EVENT_FILE_FAILURE_SIZE]) > 0 {
for _, f := range extra[config.EVENT_FILE_FAILURE_SIZE] {
fi := f.(config.FileInfo)
text := fmt.Sprintf("file %s too big to download (%#v > allowed size: %#v)", fi.Name, fi.Size, general.MediaDownloadSize)
rmsg = append(rmsg, config.Message{Text: text, Username: "<system> ", Channel: msg.Channel})
}
return rmsg
}
return rmsg
}
func GetAvatar(av map[string]string, userid string, general *config.Protocol) string {
if sha, ok := av[userid]; ok {
return general.MediaServerDownload + "/" + sha + "/" + userid + ".png"
}
return ""
}

View File

@ -5,11 +5,12 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" "github.com/42wim/matterbridge/bridge/helper"
"github.com/lrstanley/girc" "github.com/lrstanley/girc"
"github.com/paulrosania/go-charset/charset" "github.com/paulrosania/go-charset/charset"
_ "github.com/paulrosania/go-charset/data" _ "github.com/paulrosania/go-charset/data"
"github.com/saintfish/chardet" "github.com/saintfish/chardet"
log "github.com/sirupsen/logrus"
"io" "io"
"io/ioutil" "io/ioutil"
"net" "net"
@ -18,34 +19,32 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode/utf8"
) )
type Birc struct { type Birc struct {
i *girc.Client i *girc.Client
Nick string Nick string
names map[string][]string names map[string][]string
Config *config.Protocol
Remote chan config.Message
connected chan struct{} connected chan struct{}
Local chan config.Message // local queue for flood control Local chan config.Message // local queue for flood control
Account string
FirstConnection bool FirstConnection bool
*config.BridgeConfig
} }
var flog *log.Entry var flog *log.Entry
var protocol = "irc" var protocol = "irc"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Birc { func New(cfg *config.BridgeConfig) *Birc {
b := &Birc{} b := &Birc{}
b.Config = &cfg b.BridgeConfig = cfg
b.Nick = b.Config.Nick b.Nick = b.Config.Nick
b.Remote = c
b.names = make(map[string][]string) b.names = make(map[string][]string)
b.Account = account
b.connected = make(chan struct{}) b.connected = make(chan struct{})
if b.Config.MessageDelay == 0 { if b.Config.MessageDelay == 0 {
b.Config.MessageDelay = 1300 b.Config.MessageDelay = 1300
@ -81,12 +80,22 @@ func (b *Birc) Connect() error {
if err != nil { if err != nil {
return err return err
} }
// fix strict user handling of girc
user := b.Config.Nick
for !girc.IsValidUser(user) {
if len(user) == 1 {
user = "matterbridge"
break
}
user = user[1:]
}
i := girc.New(girc.Config{ i := girc.New(girc.Config{
Server: server, Server: server,
ServerPass: b.Config.Password, ServerPass: b.Config.Password,
Port: port, Port: port,
Nick: b.Config.Nick, Nick: b.Config.Nick,
User: b.Config.Nick, User: user,
Name: b.Config.Nick, Name: b.Config.Nick,
SSL: b.Config.UseTLS, SSL: b.Config.UseTLS,
TLSConfig: &tls.Config{InsecureSkipVerify: b.Config.SkipTLSVerify, ServerName: server}, TLSConfig: &tls.Config{InsecureSkipVerify: b.Config.SkipTLSVerify, ServerName: server},
@ -168,9 +177,36 @@ func (b *Birc) Send(msg config.Message) (string, error) {
msg.Text = buf.String() msg.Text = buf.String()
} }
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.Local <- rmsg
}
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" {
msg.Text = fi.URL
}
b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
}
return "", nil
}
}
// split long messages on messageLength, to avoid clipped messages #281
if b.Config.MessageSplit {
msg.Text = helper.SplitStringLength(msg.Text, b.Config.MessageLength)
}
for _, text := range strings.Split(msg.Text, "\n") { for _, text := range strings.Split(msg.Text, "\n") {
if len(text) > b.Config.MessageLength { if len(text) > b.Config.MessageLength {
text = text[:b.Config.MessageLength] + " <message clipped>" text = text[:b.Config.MessageLength-len(" <message clipped>")]
if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
text = text[:len(text)-size]
}
text += " <message clipped>"
} }
if len(b.Local) < b.Config.MessageQueue { if len(b.Local) < b.Config.MessageQueue {
if len(b.Local) == b.Config.MessageQueue-1 { if len(b.Local) == b.Config.MessageQueue-1 {
@ -241,6 +277,7 @@ func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
channel := event.Params[0] channel := event.Params[0]
if event.Command == "KICK" { if event.Command == "KICK" {
flog.Infof("Got kicked from %s by %s", channel, event.Source.Name) flog.Infof("Got kicked from %s by %s", channel, event.Source.Name)
time.Sleep(time.Duration(b.Config.RejoinDelay) * time.Second)
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS} b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
return return
} }
@ -296,14 +333,13 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
if event.Source.Name == b.Nick { if event.Source.Name == b.Nick {
return return
} }
rmsg := config.Message{Username: event.Source.Name, Channel: event.Params[0], Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host} rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host}
flog.Debugf("handlePrivMsg() %s %s %#v", event.Source.Name, event.Trailing, event) flog.Debugf("handlePrivMsg() %s %s %#v", event.Source.Name, event.Trailing, event)
msg := "" msg := ""
if event.Command == "CTCP_ACTION" { if event.IsAction() {
// msg = event.Source.Name + " "
rmsg.Event = config.EVENT_USER_ACTION rmsg.Event = config.EVENT_USER_ACTION
} }
msg += event.Trailing msg += event.StripAction()
// strip IRC colors // strip IRC colors
re := regexp.MustCompile(`[[:cntrl:]](?:\d{1,2}(?:,\d{1,2})?)?`) re := regexp.MustCompile(`[[:cntrl:]](?:\d{1,2}(?:,\d{1,2})?)?`)
msg = re.ReplaceAllString(msg, "") msg = re.ReplaceAllString(msg, "")

View File

@ -1,37 +1,36 @@
package bmatrix package bmatrix
import ( import (
"bytes"
"mime"
"regexp" "regexp"
"strings"
"sync" "sync"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" "github.com/42wim/matterbridge/bridge/helper"
matrix "github.com/matrix-org/gomatrix" log "github.com/sirupsen/logrus"
matrix "github.com/matterbridge/gomatrix"
) )
type Bmatrix struct { type Bmatrix struct {
mc *matrix.Client mc *matrix.Client
Config *config.Protocol
Remote chan config.Message
Account string
UserID string UserID string
RoomMap map[string]string RoomMap map[string]string
sync.RWMutex sync.RWMutex
*config.BridgeConfig
} }
var flog *log.Entry var flog *log.Entry
var protocol = "matrix" var protocol = "matrix"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Bmatrix { func New(cfg *config.BridgeConfig) *Bmatrix {
b := &Bmatrix{} b := &Bmatrix{BridgeConfig: cfg}
b.RoomMap = make(map[string]string) b.RoomMap = make(map[string]string)
b.Config = &cfg
b.Account = account
b.Remote = c
return b return b
} }
@ -76,19 +75,79 @@ func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
func (b *Bmatrix) Send(msg config.Message) (string, error) { func (b *Bmatrix) Send(msg config.Message) (string, error) {
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
channel := b.getRoomID(msg.Channel)
// ignore delete messages // ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE { if msg.Event == config.EVENT_MSG_DELETE {
return "", nil if msg.ID == "" {
return "", nil
}
resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
if err != nil {
return "", err
}
return resp.EventID, err
} }
channel := b.getRoomID(msg.Channel)
flog.Debugf("Sending to channel %s", channel) flog.Debugf("Sending to channel %s", channel)
if msg.Event == config.EVENT_USER_ACTION { if msg.Event == config.EVENT_USER_ACTION {
b.mc.SendMessageEvent(channel, "m.room.message", resp, err := b.mc.SendMessageEvent(channel, "m.room.message",
matrix.TextMessage{"m.emote", msg.Username + msg.Text}) matrix.TextMessage{"m.emote", msg.Username + msg.Text})
return "", nil if err != nil {
return "", err
}
return resp.EventID, err
} }
b.mc.SendText(channel, msg.Username+msg.Text)
return "", nil if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.mc.SendText(channel, rmsg.Username+rmsg.Text)
}
// check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
content := bytes.NewReader(*fi.Data)
sp := strings.Split(fi.Name, ".")
mtype := mime.TypeByExtension("." + sp[len(sp)-1])
if strings.Contains(mtype, "image") ||
strings.Contains(mtype, "video") {
if fi.Comment != "" {
_, err := b.mc.SendText(channel, msg.Username+fi.Comment)
if err != nil {
flog.Errorf("file comment failed: %#v", err)
}
}
flog.Debugf("uploading file: %s %s", fi.Name, mtype)
res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
if err != nil {
flog.Errorf("file upload failed: %#v", err)
continue
}
if strings.Contains(mtype, "video") {
flog.Debugf("sendVideo %s", res.ContentURI)
_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
if err != nil {
flog.Errorf("sendVideo failed: %#v", err)
}
}
if strings.Contains(mtype, "image") {
flog.Debugf("sendImage %s", res.ContentURI)
_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
if err != nil {
flog.Errorf("sendImage failed: %#v", err)
}
}
flog.Debugf("result: %#v", res)
}
}
return "", nil
}
}
resp, err := b.mc.SendText(channel, msg.Username+msg.Text)
if err != nil {
return "", err
}
return resp.EventID, err
} }
func (b *Bmatrix) getRoomID(channel string) string { func (b *Bmatrix) getRoomID(channel string) string {
@ -101,31 +160,11 @@ func (b *Bmatrix) getRoomID(channel string) string {
} }
return "" return ""
} }
func (b *Bmatrix) handlematrix() error { func (b *Bmatrix) handlematrix() error {
syncer := b.mc.Syncer.(*matrix.DefaultSyncer) syncer := b.mc.Syncer.(*matrix.DefaultSyncer)
syncer.OnEventType("m.room.message", func(ev *matrix.Event) { syncer.OnEventType("m.room.redaction", b.handleEvent)
if (ev.Content["msgtype"].(string) == "m.text" || ev.Content["msgtype"].(string) == "m.notice" || ev.Content["msgtype"].(string) == "m.emote") && ev.Sender != b.UserID { syncer.OnEventType("m.room.message", b.handleEvent)
b.RLock()
channel, ok := b.RoomMap[ev.RoomID]
b.RUnlock()
if !ok {
flog.Debugf("Unknown room %s", ev.RoomID)
return
}
username := ev.Sender[1:]
if b.Config.NoHomeServerSuffix {
re := regexp.MustCompile("(.*?):.*")
username = re.ReplaceAllString(username, `$1`)
}
rmsg := config.Message{Username: username, Text: ev.Content["body"].(string), Channel: channel, Account: b.Account, UserID: ev.Sender}
if ev.Content["msgtype"].(string) == "m.emote" {
rmsg.Event = config.EVENT_USER_ACTION
}
flog.Debugf("Sending message from %s on %s to gateway", ev.Sender, b.Account)
b.Remote <- rmsg
}
flog.Debugf("Received: %#v", ev)
})
go func() { go func() {
for { for {
if err := b.mc.Sync(); err != nil { if err := b.mc.Sync(); err != nil {
@ -135,3 +174,77 @@ func (b *Bmatrix) handlematrix() error {
}() }()
return nil return nil
} }
func (b *Bmatrix) handleEvent(ev *matrix.Event) {
flog.Debugf("Received: %#v", ev)
if ev.Sender != b.UserID {
b.RLock()
channel, ok := b.RoomMap[ev.RoomID]
b.RUnlock()
if !ok {
flog.Debugf("Unknown room %s", ev.RoomID)
return
}
username := ev.Sender[1:]
if b.Config.NoHomeServerSuffix {
re := regexp.MustCompile("(.*?):.*")
username = re.ReplaceAllString(username, `$1`)
}
var text string
text, _ = ev.Content["body"].(string)
rmsg := config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: ev.Sender}
rmsg.ID = ev.ID
if ev.Type == "m.room.redaction" {
rmsg.Event = config.EVENT_MSG_DELETE
rmsg.ID = ev.Redacts
rmsg.Text = config.EVENT_MSG_DELETE
b.Remote <- rmsg
return
}
if ev.Content["msgtype"].(string) == "m.emote" {
rmsg.Event = config.EVENT_USER_ACTION
}
if ev.Content["msgtype"] != nil && ev.Content["msgtype"].(string) == "m.image" ||
ev.Content["msgtype"].(string) == "m.video" ||
ev.Content["msgtype"].(string) == "m.file" {
flog.Debugf("ev: %#v", ev)
rmsg.Extra = make(map[string][]interface{})
url := ev.Content["url"].(string)
url = strings.Replace(url, "mxc://", b.Config.Server+"/_matrix/media/v1/download/", -1)
info := ev.Content["info"].(map[string]interface{})
size := info["size"].(float64)
name := ev.Content["body"].(string)
// check if we have an image uploaded without extension
if !strings.Contains(name, ".") {
if ev.Content["msgtype"].(string) == "m.image" {
if mtype, ok := ev.Content["mimetype"].(string); ok {
mext, _ := mime.ExtensionsByType(mtype)
if len(mext) > 0 {
name = name + mext[0]
}
} else {
// just a default .png extension if we don't have mime info
name = name + ".png"
}
}
}
flog.Debugf("trying to download %#v with size %#v", name, size)
if size <= float64(b.General.MediaDownloadSize) {
data, err := helper.DownloadFile(url)
if err != nil {
flog.Errorf("download %s failed %#v", url, err)
} else {
flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url))
rmsg.Extra["file"] = append(rmsg.Extra["file"], config.FileInfo{Name: name, Data: data})
}
} else {
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, size, b.General.MediaDownloadSize)
rmsg.Event = config.EVENT_FILE_FAILURE_SIZE
rmsg.Extra[rmsg.Event] = append(rmsg.Extra[rmsg.Event], config.FileInfo{Name: name, Size: int64(size)})
}
rmsg.Text = ""
}
flog.Debugf("Sending message from %s on %s to gateway", ev.Sender, b.Account)
b.Remote <- rmsg
}
}

View File

@ -4,9 +4,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient" "github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
"strings" "strings"
) )
@ -32,24 +33,20 @@ type MMMessage struct {
type Bmattermost struct { type Bmattermost struct {
MMhook MMhook
MMapi MMapi
Config *config.Protocol TeamId string
Remote chan config.Message *config.BridgeConfig
TeamId string avatarMap map[string]string
Account string
} }
var flog *log.Entry var flog *log.Entry
var protocol = "mattermost" var protocol = "mattermost"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Bmattermost { func New(cfg *config.BridgeConfig) *Bmattermost {
b := &Bmattermost{} b := &Bmattermost{BridgeConfig: cfg, avatarMap: make(map[string]string)}
b.Config = &cfg
b.Remote = c
b.Account = account
b.mmMap = make(map[string]string) b.mmMap = make(map[string]string)
return b return b
} }
@ -153,17 +150,46 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
message := msg.Text message := msg.Text
channel := msg.Channel channel := msg.Channel
// map the file SHA to our user (caches the avatar)
if msg.Event == config.EVENT_AVATAR_DOWNLOAD {
fi := msg.Extra["file"][0].(config.FileInfo)
/* if we have a sha we have successfully uploaded the file to the media server,
so we can now cache the sha */
if fi.SHA != "" {
flog.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
b.avatarMap[msg.UserID] = fi.SHA
}
return "", nil
}
if b.Config.PrefixMessagesWithNick { if b.Config.PrefixMessagesWithNick {
message = nick + message message = nick + message
} }
if b.Config.WebhookURL != "" { if b.Config.WebhookURL != "" {
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL, Channel: channel, UserName: rmsg.Username,
Text: rmsg.Text, Props: make(map[string]interface{})}
matterMessage.Props["matterbridge"] = true
b.mh.Send(matterMessage)
}
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
message += fi.URL
}
}
}
}
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL} matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.IconURL = msg.Avatar matterMessage.IconURL = msg.Avatar
matterMessage.Channel = channel matterMessage.Channel = channel
matterMessage.UserName = nick matterMessage.UserName = nick
matterMessage.Type = "" matterMessage.Type = ""
matterMessage.Text = message matterMessage.Text = message
matterMessage.Text = message
matterMessage.Props = make(map[string]interface{}) matterMessage.Props = make(map[string]interface{})
matterMessage.Props["matterbridge"] = true matterMessage.Props["matterbridge"] = true
err := b.mh.Send(matterMessage) err := b.mh.Send(matterMessage)
@ -180,6 +206,9 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
return msg.ID, b.mc.DeleteMessage(msg.ID) return msg.ID, b.mc.DeleteMessage(msg.ID)
} }
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.mc.PostMessage(b.mc.GetChannelId(channel, ""), rmsg.Username+rmsg.Text)
}
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
var err error var err error
var res, id string var res, id string
@ -190,9 +219,9 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
flog.Debugf("ERROR %#v", err) flog.Debugf("ERROR %#v", err)
return "", err return "", err
} }
message = "uploaded a file: " + fi.Name message = fi.Comment
if b.Config.PrefixMessagesWithNick { if b.Config.PrefixMessagesWithNick {
message = nick + "uploaded a file: " + fi.Name message = nick + fi.Comment
} }
res, err = b.mc.PostMessageWithFiles(b.mc.GetChannelId(channel, ""), message, []string{id}) res, err = b.mc.PostMessageWithFiles(b.mc.GetChannelId(channel, ""), message, []string{id})
} }
@ -219,7 +248,8 @@ func (b *Bmattermost) handleMatter() {
go b.handleMatterClient(mchan) go b.handleMatterClient(mchan)
} }
for message := range mchan { for message := range mchan {
rmsg := config.Message{Username: message.Username, Channel: message.Channel, Account: b.Account, UserID: message.UserID, ID: message.ID, Event: message.Event, Extra: message.Extra} avatar := helper.GetAvatar(b.avatarMap, message.UserID, b.General)
rmsg := config.Message{Username: message.Username, Channel: message.Channel, Account: b.Account, UserID: message.UserID, ID: message.ID, Event: message.Event, Extra: message.Extra, Avatar: avatar}
text, ok := b.replaceAction(message.Text) text, ok := b.replaceAction(message.Text)
if ok { if ok {
rmsg.Event = config.EVENT_USER_ACTION rmsg.Event = config.EVENT_USER_ACTION
@ -245,6 +275,11 @@ func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
continue continue
} }
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" {
b.handleDownloadAvatar(message.UserID, message.Channel)
}
m := &MMMessage{Extra: make(map[string][]interface{})} m := &MMMessage{Extra: make(map[string][]interface{})}
props := message.Post.Props props := message.Post.Props
@ -281,8 +316,26 @@ func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
m.Event = config.EVENT_MSG_DELETE m.Event = config.EVENT_MSG_DELETE
} }
if len(message.Post.FileIds) > 0 { if len(message.Post.FileIds) > 0 {
for _, link := range b.mc.GetFileLinks(message.Post.FileIds) { for _, id := range message.Post.FileIds {
m.Text = m.Text + "\n" + link url, _ := b.mc.Client.GetFileLink(id)
finfo, resp := b.mc.Client.GetFileInfo(id)
if resp.Error != nil {
continue
}
flog.Debugf("trying to download %#v fileid %#v with size %#v", finfo.Name, finfo.Id, finfo.Size)
if int(finfo.Size) > b.General.MediaDownloadSize {
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", finfo.Name, finfo.Size, b.General.MediaDownloadSize)
m.Event = config.EVENT_FILE_FAILURE_SIZE
m.Extra[m.Event] = append(m.Extra[m.Event], config.FileInfo{Name: finfo.Name, Comment: message.Text, Size: int64(finfo.Size)})
continue
}
data, resp := b.mc.Client.DownloadFile(id, true)
if resp.Error != nil {
flog.Errorf("download %s failed %#v", finfo.Name, resp.Error)
continue
}
flog.Debugf("download OK %#v %#v", finfo.Name, len(data))
m.Extra["file"] = append(m.Extra["file"], config.FileInfo{Name: finfo.Name, Data: &data, URL: url, Comment: message.Text})
} }
} }
mchan <- m mchan <- m
@ -311,6 +364,9 @@ func (b *Bmattermost) apiLogin() error {
b.mc = matterclient.New(b.Config.Login, password, b.mc = matterclient.New(b.Config.Login, password,
b.Config.Team, b.Config.Server) b.Config.Team, b.Config.Server)
if b.General.Debug {
b.mc.SetLogLevel("debug")
}
b.mc.SkipTLSVerify = b.Config.SkipTLSVerify b.mc.SkipTLSVerify = b.Config.SkipTLSVerify
b.mc.NoTLS = b.Config.NoTLS b.mc.NoTLS = b.Config.NoTLS
flog.Infof("Connecting %s (team: %s) on %s", b.Config.Login, b.Config.Team, b.Config.Server) flog.Infof("Connecting %s (team: %s) on %s", b.Config.Login, b.Config.Team, b.Config.Server)
@ -331,3 +387,27 @@ func (b *Bmattermost) replaceAction(text string) (string, bool) {
} }
return text, false return text, false
} }
// handleDownloadAvatar downloads the avatar of userid from channel
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
// logs an error message if it fails
func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
var name string
msg := config.Message{Username: "system", Text: "avatar", Channel: channel, Account: b.Account, UserID: userid, Event: config.EVENT_AVATAR_DOWNLOAD, Extra: make(map[string][]interface{})}
if _, ok := b.avatarMap[userid]; !ok {
data, resp := b.mc.Client.GetProfileImage(userid, "")
if resp.Error != nil {
flog.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
}
if len(data) <= b.General.MediaDownloadSize {
name = userid + ".png"
flog.Debugf("download OK %#v %#v", name, len(data))
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: &data, Avatar: true})
flog.Debugf("Sending avatar download message from %#v on %s to gateway", userid, b.Account)
flog.Debugf("Message is %#v", msg)
b.Remote <- msg
} else {
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, len(data), b.General.MediaDownloadSize)
}
}
}

View File

@ -2,9 +2,10 @@ package brocketchat
import ( import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/hook/rockethook" "github.com/42wim/matterbridge/hook/rockethook"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
type MMhook struct { type MMhook struct {
@ -14,24 +15,18 @@ type MMhook struct {
type Brocketchat struct { type Brocketchat struct {
MMhook MMhook
Config *config.Protocol *config.BridgeConfig
Remote chan config.Message
Account string
} }
var flog *log.Entry var flog *log.Entry
var protocol = "rocketchat" var protocol = "rocketchat"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Brocketchat { func New(cfg *config.BridgeConfig) *Brocketchat {
b := &Brocketchat{} return &Brocketchat{BridgeConfig: cfg}
b.Config = &cfg
b.Remote = c
b.Account = account
return b
} }
func (b *Brocketchat) Command(cmd string) string { func (b *Brocketchat) Command(cmd string) string {
@ -63,6 +58,22 @@ func (b *Brocketchat) Send(msg config.Message) (string, error) {
return "", nil return "", nil
} }
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL, Channel: rmsg.Channel, UserName: rmsg.Username,
Text: rmsg.Text}
b.mh.Send(matterMessage)
}
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text += fi.URL
}
}
}
}
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL} matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.Channel = msg.Channel matterMessage.Channel = msg.Channel
matterMessage.UserName = msg.Username matterMessage.UserName = msg.Username

View File

@ -5,9 +5,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/matterbridge/slack" "github.com/nlopes/slack"
"html" "html"
"io" "io"
"net/http" "net/http"
@ -27,29 +28,23 @@ type MMMessage struct {
type Bslack struct { type Bslack struct {
mh *matterhook.Client mh *matterhook.Client
sc *slack.Client sc *slack.Client
Config *config.Protocol
rtm *slack.RTM rtm *slack.RTM
Plus bool Plus bool
Remote chan config.Message
Users []slack.User Users []slack.User
Account string
si *slack.Info si *slack.Info
channels []slack.Channel channels []slack.Channel
*config.BridgeConfig
} }
var flog *log.Entry var flog *log.Entry
var protocol = "slack" var protocol = "slack"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Bslack { func New(cfg *config.BridgeConfig) *Bslack {
b := &Bslack{} return &Bslack{BridgeConfig: cfg}
b.Config = &cfg
b.Remote = c
b.Account = account
return b
} }
func (b *Bslack) Command(cmd string) string { func (b *Bslack) Command(cmd string) string {
@ -113,7 +108,7 @@ func (b *Bslack) Disconnect() error {
func (b *Bslack) JoinChannel(channel config.ChannelInfo) error { func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
// we can only join channels using the API // we can only join channels using the API
if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" { if b.sc != nil {
if strings.HasPrefix(b.Config.Token, "xoxb") { if strings.HasPrefix(b.Config.Token, "xoxb") {
// TODO check if bot has already joined channel // TODO check if bot has already joined channel
return nil return nil
@ -140,6 +135,22 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
message = nick + " " + message message = nick + " " + message
} }
if b.Config.WebhookURL != "" { if b.Config.WebhookURL != "" {
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL, Channel: channel, UserName: rmsg.Username,
Text: rmsg.Text}
b.mh.Send(matterMessage)
}
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
message += fi.URL
}
}
}
}
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL} matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.Channel = channel matterMessage.Channel = channel
matterMessage.UserName = nick matterMessage.UserName = nick
@ -161,7 +172,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
np.AsUser = true np.AsUser = true
} }
np.Username = nick np.Username = nick
np.IconURL = config.GetIconURL(&msg, b.Config) np.IconURL = config.GetIconURL(&msg, &b.Config)
if msg.Avatar != "" { if msg.Avatar != "" {
np.IconURL = msg.Avatar np.IconURL = msg.Avatar
} }
@ -189,15 +200,19 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
} }
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.sc.PostMessage(schannel.ID, rmsg.Username+rmsg.Text, np)
}
// check if we have files to upload (from slack, telegram or mattermost) // check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
var err error var err error
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)
_, err = b.sc.UploadFile(slack.FileUploadParameters{ _, err = b.sc.UploadFile(slack.FileUploadParameters{
Reader: bytes.NewReader(*fi.Data), Reader: bytes.NewReader(*fi.Data),
Filename: fi.Name, Filename: fi.Name,
Channels: []string{schannel.ID}, Channels: []string{schannel.ID},
InitialComment: fi.Comment,
}) })
if err != nil { if err != nil {
flog.Errorf("uploadfile %#v", err) flog.Errorf("uploadfile %#v", err)
@ -289,16 +304,29 @@ func (b *Bslack) handleSlack() {
msg.Event = config.EVENT_MSG_DELETE msg.Event = config.EVENT_MSG_DELETE
msg.ID = "slack " + message.Raw.DeletedTimestamp msg.ID = "slack " + message.Raw.DeletedTimestamp
} }
if message.Raw.SubType == "channel_topic" || message.Raw.SubType == "channel_purpose" {
msg.Event = config.EVENT_TOPIC_CHANGE
}
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra // if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
if message.Raw.File != nil { if message.Raw.File != nil {
// limit to 1MB for now // limit to 1MB for now
if message.Raw.File.Size <= 1000000 { comment := ""
results := regexp.MustCompile(`.*?commented: (.*)`).FindAllStringSubmatch(msg.Text, -1)
if len(results) > 0 {
comment = results[0][1]
}
if message.Raw.File.Size > b.General.MediaDownloadSize {
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", message.Raw.File.Name, message.Raw.File.Size, b.General.MediaDownloadSize)
msg.Event = config.EVENT_FILE_FAILURE_SIZE
msg.Extra[msg.Event] = append(msg.Extra[msg.Event], config.FileInfo{Name: message.Raw.File.Name, Comment: comment, Size: int64(message.Raw.File.Size)})
} else {
data, err := b.downloadFile(message.Raw.File.URLPrivateDownload) data, err := b.downloadFile(message.Raw.File.URLPrivateDownload)
if err != nil { if err != nil {
flog.Errorf("download %s failed %#v", message.Raw.File.URLPrivateDownload, err) flog.Errorf("download %s failed %#v", message.Raw.File.URLPrivateDownload, err)
} else { } else {
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: message.Raw.File.Name, Data: data}) msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: message.Raw.File.Name, Data: data, Comment: comment})
} }
} }
} }
@ -309,9 +337,14 @@ func (b *Bslack) handleSlack() {
func (b *Bslack) handleSlackClient(mchan chan *MMMessage) { func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
for msg := range b.rtm.IncomingEvents { for msg := range b.rtm.IncomingEvents {
if msg.Type != "user_typing" && msg.Type != "latency_report" {
flog.Debugf("Receiving from slackclient %#v", msg.Data)
}
switch ev := msg.Data.(type) { switch ev := msg.Data.(type) {
case *slack.MessageEvent: case *slack.MessageEvent:
flog.Debugf("Receiving from slackclient %#v", ev) if ev.SubType == "pinned_item" || ev.SubType == "unpinned_item" {
continue
}
if len(ev.Attachments) > 0 { if len(ev.Attachments) > 0 {
// skip messages we made ourselves // skip messages we made ourselves
if ev.Attachments[0].CallbackID == "matterbridge" { if ev.Attachments[0].CallbackID == "matterbridge" {
@ -335,7 +368,7 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
continue continue
} }
m := &MMMessage{} m := &MMMessage{}
if ev.BotID == "" && ev.SubType != "message_deleted" { if ev.BotID == "" && ev.SubType != "message_deleted" && ev.SubType != "file_comment" {
user, err := b.rtm.GetUserInfo(ev.User) user, err := b.rtm.GetUserInfo(ev.User)
if err != nil { if err != nil {
continue continue
@ -375,6 +408,11 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
m.UserID = bot.ID m.UserID = bot.ID
} }
} }
if ev.SubType == "file_comment" {
m.Username = "system"
}
mchan <- m mchan <- m
case *slack.OutgoingErrorEvent: case *slack.OutgoingErrorEvent:
flog.Debugf("%#v", ev.Error()) flog.Debugf("%#v", ev.Error())
@ -394,6 +432,8 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
} }
case *slack.InvalidAuthEvent: case *slack.InvalidAuthEvent:
flog.Fatalf("Invalid Token %#v", ev) flog.Fatalf("Invalid Token %#v", ev)
case *slack.ConnectionErrorEvent:
flog.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
default: default:
} }
} }

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

@ -0,0 +1,139 @@
package bsshchat
import (
"bufio"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
log "github.com/sirupsen/logrus"
"github.com/shazow/ssh-chat/sshd"
"io"
"strings"
)
type Bsshchat struct {
r *bufio.Scanner
w io.WriteCloser
*config.BridgeConfig
}
var flog *log.Entry
var protocol = "sshchat"
func init() {
flog = log.WithFields(log.Fields{"prefix": protocol})
}
func New(cfg *config.BridgeConfig) *Bsshchat {
return &Bsshchat{BridgeConfig: cfg}
}
func (b *Bsshchat) Connect() error {
var err error
flog.Infof("Connecting %s", b.Config.Server)
go func() {
err = sshd.ConnectShell(b.Config.Server, b.Config.Nick, func(r io.Reader, w io.WriteCloser) error {
b.r = bufio.NewScanner(r)
b.w = w
b.r.Scan()
w.Write([]byte("/theme mono\r\n"))
b.handleSshChat()
return nil
})
}()
if err != nil {
flog.Debugf("%#v", err)
return err
}
flog.Info("Connection succeeded")
return nil
}
func (b *Bsshchat) Disconnect() error {
return nil
}
func (b *Bsshchat) JoinChannel(channel config.ChannelInfo) error {
return nil
}
func (b *Bsshchat) Send(msg config.Message) (string, error) {
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
flog.Debugf("Receiving %#v", msg)
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.w.Write([]byte(rmsg.Username + rmsg.Text + "\r\n"))
}
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" {
msg.Text = fi.URL
}
b.w.Write([]byte(msg.Username + msg.Text))
}
return "", nil
}
}
b.w.Write([]byte(msg.Username + msg.Text + "\r\n"))
return "", nil
}
/*
func (b *Bsshchat) sshchatKeepAlive() chan bool {
done := make(chan bool)
go func() {
ticker := time.NewTicker(90 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
flog.Debugf("PING")
err := b.xc.PingC2S("", "")
if err != nil {
flog.Debugf("PING failed %#v", err)
}
case <-done:
return
}
}
}()
return done
}
*/
func stripPrompt(s string) string {
pos := strings.LastIndex(s, "\033[K")
if pos < 0 {
return s
}
return s[pos+3:]
}
func (b *Bsshchat) handleSshChat() error {
/*
done := b.sshchatKeepAlive()
defer close(done)
*/
wait := true
for {
if b.r.Scan() {
res := strings.Split(stripPrompt(b.r.Text()), ":")
if res[0] == "-> Set theme" {
wait = false
log.Debugf("mono found, allowing")
continue
}
if !wait {
flog.Debugf("message %#v", res)
rmsg := config.Message{Username: res[0], Text: strings.Join(res[1:], ":"), Channel: "sshchat", Account: b.Account, UserID: "nick"}
b.Remote <- rmsg
}
}
}
}

View File

@ -6,7 +6,7 @@ import (
"github.com/Philipp15b/go-steam" "github.com/Philipp15b/go-steam"
"github.com/Philipp15b/go-steam/protocol/steamlang" "github.com/Philipp15b/go-steam/protocol/steamlang"
"github.com/Philipp15b/go-steam/steamid" "github.com/Philipp15b/go-steam/steamid"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
//"io/ioutil" //"io/ioutil"
"strconv" "strconv"
"sync" "sync"
@ -16,25 +16,20 @@ import (
type Bsteam struct { type Bsteam struct {
c *steam.Client c *steam.Client
connected chan struct{} connected chan struct{}
Config *config.Protocol
Remote chan config.Message
Account string
userMap map[steamid.SteamId]string userMap map[steamid.SteamId]string
sync.RWMutex sync.RWMutex
*config.BridgeConfig
} }
var flog *log.Entry var flog *log.Entry
var protocol = "steam" var protocol = "steam"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Bsteam { func New(cfg *config.BridgeConfig) *Bsteam {
b := &Bsteam{} b := &Bsteam{BridgeConfig: cfg}
b.Config = &cfg
b.Remote = c
b.Account = account
b.userMap = make(map[steamid.SteamId]string) b.userMap = make(map[steamid.SteamId]string)
b.connected = make(chan struct{}) b.connected = make(chan struct{})
return b return b

View File

@ -3,33 +3,29 @@ package btelegram
import ( import (
"regexp" "regexp"
"strconv" "strconv"
"strings"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
log "github.com/Sirupsen/logrus"
"github.com/go-telegram-bot-api/telegram-bot-api" "github.com/go-telegram-bot-api/telegram-bot-api"
log "github.com/sirupsen/logrus"
) )
type Btelegram struct { type Btelegram struct {
c *tgbotapi.BotAPI c *tgbotapi.BotAPI
Config *config.Protocol *config.BridgeConfig
Remote chan config.Message avatarMap map[string]string // keep cache of userid and avatar sha
Account string
} }
var flog *log.Entry var flog *log.Entry
var protocol = "telegram" var protocol = "telegram"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Btelegram { func New(cfg *config.BridgeConfig) *Btelegram {
b := &Btelegram{} return &Btelegram{BridgeConfig: cfg, avatarMap: make(map[string]string)}
b.Config = &cfg
b.Remote = c
b.Account = account
return b
} }
func (b *Btelegram) Connect() error { func (b *Btelegram) Connect() error {
@ -40,7 +36,9 @@ func (b *Btelegram) Connect() error {
flog.Debugf("%#v", err) flog.Debugf("%#v", err)
return err return err
} }
updates, err := b.c.GetUpdatesChan(tgbotapi.NewUpdate(0)) u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates, err := b.c.GetUpdatesChan(u)
if err != nil { if err != nil {
flog.Debugf("%#v", err) flog.Debugf("%#v", err)
return err return err
@ -66,6 +64,18 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
return "", err return "", err
} }
// map the file SHA to our user (caches the avatar)
if msg.Event == config.EVENT_AVATAR_DOWNLOAD {
fi := msg.Extra["file"][0].(config.FileInfo)
/* if we have a sha we have successfully uploaded the file to the media server,
so we can now cache the sha */
if fi.SHA != "" {
flog.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
b.avatarMap[msg.UserID] = fi.SHA
}
return "", nil
}
if b.Config.MessageFormat == "HTML" { if b.Config.MessageFormat == "HTML" {
msg.Text = makeHTML(msg.Text) msg.Text = makeHTML(msg.Text)
} }
@ -89,6 +99,14 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
return "", err return "", err
} }
m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text) m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
if b.Config.MessageFormat == "HTML" {
flog.Debug("Using mode HTML")
m.ParseMode = tgbotapi.ModeHTML
}
if b.Config.MessageFormat == "Markdown" {
flog.Debug("Using mode markdown")
m.ParseMode = tgbotapi.ModeMarkdown
}
_, err = b.c.Send(m) _, err = b.c.Send(m)
if err != nil { if err != nil {
return "", err return "", err
@ -97,6 +115,9 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
} }
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.sendMessage(chatid, rmsg.Username+rmsg.Text)
}
// check if we have files to upload (from slack, telegram or mattermost) // check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
var c tgbotapi.Chattable var c tgbotapi.Chattable
@ -113,25 +134,23 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
if err != nil { if err != nil {
log.Errorf("file upload failed: %#v", err) log.Errorf("file upload failed: %#v", err)
} }
if fi.Comment != "" {
b.sendMessage(chatid, msg.Username+fi.Comment)
}
} }
return "", nil
} }
} }
return b.sendMessage(chatid, msg.Username+msg.Text)
m := tgbotapi.NewMessage(chatid, msg.Username+msg.Text)
if b.Config.MessageFormat == "HTML" {
m.ParseMode = tgbotapi.ModeHTML
}
res, err := b.c.Send(m)
if err != nil {
return "", err
}
return strconv.Itoa(res.MessageID), nil
} }
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
for update := range updates { for update := range updates {
flog.Debugf("Receiving from telegram: %#v", update.Message) flog.Debugf("Receiving from telegram: %#v", update.Message)
if update.Message == nil {
flog.Error("Getting nil messages, this shouldn't happen.")
continue
}
var message *tgbotapi.Message var message *tgbotapi.Message
username := "" username := ""
channel := "" channel := ""
@ -167,23 +186,54 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
} }
text = message.Text text = message.Text
channel = strconv.FormatInt(message.Chat.ID, 10) channel = strconv.FormatInt(message.Chat.ID, 10)
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" {
b.handleDownloadAvatar(message.From.ID, channel)
}
} }
if username == "" { if username == "" {
username = "unknown" username = "unknown"
} }
if message.Sticker != nil { if message.Sticker != nil {
b.handleDownload(message.Sticker, &fmsg) b.handleDownload(message.Sticker, message.Caption, &fmsg)
} }
if message.Video != nil { if message.Video != nil {
b.handleDownload(message.Video, &fmsg) b.handleDownload(message.Video, message.Caption, &fmsg)
} }
if message.Photo != nil && b.Config.UseInsecureURL { if message.Photo != nil {
b.handleDownload(message.Photo, &fmsg) b.handleDownload(message.Photo, message.Caption, &fmsg)
} }
if message.Document != nil && b.Config.UseInsecureURL { if message.Document != nil {
b.handleDownload(message.Sticker, &fmsg) b.handleDownload(message.Document, message.Caption, &fmsg)
text = text + " " + message.Document.FileName + " : " + b.getFileDirectURL(message.Document.FileID) }
if message.Voice != nil {
b.handleDownload(message.Voice, message.Caption, &fmsg)
}
if message.Audio != nil {
b.handleDownload(message.Audio, message.Caption, &fmsg)
}
// If UseInsecureURL is used we'll have a text in fmsg.Text
if fmsg.Text != "" {
text = text + fmsg.Text
}
if message.ForwardFrom != nil {
usernameForward := ""
if b.Config.UseFirstName {
usernameForward = message.ForwardFrom.FirstName
}
if usernameForward == "" {
usernameForward = message.ForwardFrom.UserName
if usernameForward == "" {
usernameForward = message.ForwardFrom.FirstName
}
}
if usernameForward == "" {
usernameForward = "unknown"
}
text = "Forwarded from " + usernameForward + ": " + text
} }
// quote the previous message // quote the previous message
@ -207,8 +257,9 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
} }
if text != "" || len(fmsg.Extra) > 0 { if text != "" || len(fmsg.Extra) > 0 {
avatar := helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
flog.Debugf("Sending message from %s on %s to gateway", username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", username, b.Account)
msg := config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID), ID: strconv.Itoa(message.MessageID)} msg := config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID), ID: strconv.Itoa(message.MessageID), Extra: fmsg.Extra, Avatar: avatar}
flog.Debugf("Message is %#v", msg) flog.Debugf("Message is %#v", msg)
b.Remote <- msg b.Remote <- msg
} }
@ -223,46 +274,129 @@ func (b *Btelegram) getFileDirectURL(id string) string {
return res return res
} }
func (b *Btelegram) handleDownload(file interface{}, msg *config.Message) { // handleDownloadAvatar downloads the avatar of userid from channel
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
// logs an error message if it fails
func (b *Btelegram) handleDownloadAvatar(userid int, channel string) {
msg := config.Message{Username: "system", Text: "avatar", Channel: channel, Account: b.Account, UserID: strconv.Itoa(userid), Event: config.EVENT_AVATAR_DOWNLOAD, Extra: make(map[string][]interface{})}
if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok {
photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
if err != nil {
flog.Errorf("Userprofile download failed for %#v %s", userid, err)
}
if len(photos.Photos) > 0 {
photo := photos.Photos[0][0]
url := b.getFileDirectURL(photo.FileID)
name := strconv.Itoa(userid) + ".png"
flog.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
if photo.FileSize <= b.General.MediaDownloadSize {
data, err := helper.DownloadFile(url)
if err != nil {
flog.Errorf("download %s failed %#v", url, err)
} else {
flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url))
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data, Avatar: true})
flog.Debugf("Sending avatar download message from %#v on %s to gateway", userid, b.Account)
flog.Debugf("Message is %#v", msg)
b.Remote <- msg
}
} else {
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, photo.FileSize, b.General.MediaDownloadSize)
}
}
}
}
func (b *Btelegram) handleDownload(file interface{}, comment string, msg *config.Message) {
size := 0 size := 0
url := "" url := ""
name := "" name := ""
text := "" text := ""
fileid := ""
switch v := file.(type) { switch v := file.(type) {
case *tgbotapi.Audio:
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
text = " " + url
fileid = v.FileID
case *tgbotapi.Voice:
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
text = " " + url
if !strings.HasSuffix(name, ".ogg") {
name = name + ".ogg"
}
fileid = v.FileID
case *tgbotapi.Sticker: case *tgbotapi.Sticker:
size = v.FileSize size = v.FileSize
url = b.getFileDirectURL(v.FileID) url = b.getFileDirectURL(v.FileID)
name = "sticker" urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
if !strings.HasSuffix(name, ".webp") {
name = name + ".webp"
}
text = " " + url text = " " + url
fileid = v.FileID
case *tgbotapi.Video: case *tgbotapi.Video:
size = v.FileSize size = v.FileSize
url = b.getFileDirectURL(v.FileID) url = b.getFileDirectURL(v.FileID)
name = "video" urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
text = " " + url text = " " + url
fileid = v.FileID
case *[]tgbotapi.PhotoSize: case *[]tgbotapi.PhotoSize:
photos := *v photos := *v
size = photos[len(photos)-1].FileSize size = photos[len(photos)-1].FileSize
url = b.getFileDirectURL(photos[len(photos)-1].FileID) url = b.getFileDirectURL(photos[len(photos)-1].FileID)
name = "photo" urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
text = " " + url text = " " + url
case *tgbotapi.Document: case *tgbotapi.Document:
size = v.FileSize size = v.FileSize
url = b.getFileDirectURL(v.FileID) url = b.getFileDirectURL(v.FileID)
name = v.FileName name = v.FileName
text = " " + v.FileName + " : " + url text = " " + v.FileName + " : " + url
fileid = v.FileID
} }
if b.Config.UseInsecureURL { if b.Config.UseInsecureURL {
flog.Debugf("Setting message text to :%s", text)
msg.Text = text msg.Text = text
return return
} }
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra // if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
// limit to 1MB for now flog.Debugf("trying to download %#v fileid %#v with size %#v", name, fileid, size)
if size <= 1000000 { if size <= b.General.MediaDownloadSize {
data, err := helper.DownloadFile(url) data, err := helper.DownloadFile(url)
if err != nil { if err != nil {
flog.Errorf("download %s failed %#v", url, err) flog.Errorf("download %s failed %#v", url, err)
} else { } else {
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data}) flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url))
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data, Comment: comment})
} }
} else {
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, size, b.General.MediaDownloadSize)
msg.Event = config.EVENT_FILE_FAILURE_SIZE
msg.Extra[msg.Event] = append(msg.Extra[msg.Event], config.FileInfo{Name: name, Comment: comment, Size: int64(size)})
} }
} }
func (b *Btelegram) sendMessage(chatid int64, text string) (string, error) {
m := tgbotapi.NewMessage(chatid, text)
if b.Config.MessageFormat == "HTML" {
flog.Debug("Using mode HTML")
m.ParseMode = tgbotapi.ModeHTML
}
if b.Config.MessageFormat == "Markdown" {
flog.Debug("Using mode markdown")
m.ParseMode = tgbotapi.ModeMarkdown
}
res, err := b.c.Send(m)
if err != nil {
return "", err
}
return strconv.Itoa(res.MessageID), nil
}

View File

@ -3,7 +3,8 @@ package bxmpp
import ( import (
"crypto/tls" "crypto/tls"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" "github.com/42wim/matterbridge/bridge/helper"
log "github.com/sirupsen/logrus"
"github.com/jpillora/backoff" "github.com/jpillora/backoff"
"github.com/mattn/go-xmpp" "github.com/mattn/go-xmpp"
@ -14,24 +15,19 @@ import (
type Bxmpp struct { type Bxmpp struct {
xc *xmpp.Client xc *xmpp.Client
xmppMap map[string]string xmppMap map[string]string
Config *config.Protocol *config.BridgeConfig
Remote chan config.Message
Account string
} }
var flog *log.Entry var flog *log.Entry
var protocol = "xmpp" var protocol = "xmpp"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Bxmpp { func New(cfg *config.BridgeConfig) *Bxmpp {
b := &Bxmpp{} b := &Bxmpp{BridgeConfig: cfg}
b.xmppMap = make(map[string]string) b.xmppMap = make(map[string]string)
b.Config = &cfg
b.Account = account
b.Remote = c
return b return b
} }
@ -85,6 +81,25 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
return "", nil return "", nil
} }
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: rmsg.Channel + "@" + b.Config.Muc, Text: rmsg.Username + rmsg.Text})
}
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" {
msg.Text += fi.URL
}
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: msg.Username + msg.Text})
}
return "", nil
}
}
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: msg.Username + msg.Text}) b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: msg.Username + msg.Text})
return "", nil return "", nil
} }
@ -102,7 +117,7 @@ func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
TLSConfig: tc, TLSConfig: tc,
//StartTLS: false, //StartTLS: false,
Debug: true, Debug: b.General.Debug,
Session: true, Session: true,
Status: "", Status: "",
StatusMessage: "", StatusMessage: "",
@ -158,7 +173,7 @@ func (b *Bxmpp) handleXmpp() error {
if len(s) == 2 { if len(s) == 2 {
nick = s[1] nick = s[1]
} }
if nick != b.Config.Nick && v.Stamp == nodelay && v.Text != "" { if nick != b.Config.Nick && v.Stamp == nodelay && v.Text != "" && !strings.Contains(v.Text, "</subject>") {
rmsg := config.Message{Username: nick, Text: v.Text, Channel: channel, Account: b.Account, UserID: v.Remote} rmsg := config.Message{Username: nick, Text: v.Text, Channel: channel, Account: b.Account, UserID: v.Remote}
rmsg.Text, ok = b.replaceAction(rmsg.Text) rmsg.Text, ok = b.replaceAction(rmsg.Text)
if ok { if ok {

View File

@ -1,3 +1,97 @@
# v1.8.0
## New features
* general: Send chat notification if media is too big to be re-uploaded to MediaServer. See #359
* general: Download (and upload) avatar images from mattermost and telegram when mediaserver is configured. Closes #362
* general: Add label support in RemoteNickFormat
* general: Prettier info/debug log output
* mattermost: Download files and reupload to supported bridges (mattermost). Closes #357
* slack: Add ShowTopicChange option. Allow/disable topic change messages (currently only from slack). Closes #353
* slack: Add support for file comments (slack). Closes #346
* telegram: Add comment to file upload from telegram. Show comments on all bridges. Closes #358
* telegram: Add markdown support (telegram). #355
* api: Give api access to whole config.Message (and events). Closes #374
## Bugfix
* discord: Check for a valid WebhookURL (discord). Closes #367
* discord: Fix role mention replace issues
* irc: Truncate messages sent to IRC based on byte count (#368)
* mattermost: Add file download urls also to mattermost webhooks #356
* telegram: Fix panic on nil messages (telegram). Closes #366
* telegram: Fix the UseInsecureURL text (telegram). Closes #184
# v1.7.1
## Bugfix
* telegram: Enable Long Polling for Telegram. Reduces bandwidth consumption. (#350)
# v1.7.0
## New features
* matrix: Add support for deleting messages from/to matrix (matrix). Closes #320
* xmpp: Ignore <subject> messages (xmpp). #272
* irc: Add twitch support (irc) to README / wiki
## Bugfix
* general: Change RemoteNickFormat replacement order. Closes #336
* general: Make edits/delete work for bridges that gets reused. Closes #342
* general: Lowercase irc channels in config. Closes #348
* matrix: Fix possible panics (matrix). Closes #333
* matrix: Add an extension to images without one (matrix). #331
* api: Obey the Gateway value from the json (api). Closes #344
* xmpp: Print only debug messages when specified (xmpp). Closes #345
* xmpp: Allow xmpp to receive the extra messages (file uploads) when text is empty. #295
# v1.6.3
## Bugfix
* slack: Fix connection issues
* slack: Add more debug messages
* irc: Convert received IRC channel names to lowercase. Fixes #329 (#330)
# v1.6.2
## Bugfix
* mattermost: Crashes while connecting to Mattermost (regression). Closes #327
# v1.6.1
## Bugfix
* general: Display of nicks not longer working (regression). Closes #323
# v1.6.0
## New features
* sshchat: New protocol support added (https://github.com/shazow/ssh-chat)
* general: Allow specifying maximum download size of media using MediaDownloadSize (slack,telegram,matrix)
* api: Add (simple, one listener) long-polling support (api). Closes #307
* telegram: Add support for forwarded messages. Closes #313
* telegram: Add support for Audio/Voice files (telegram). Closes #314
* irc: Add RejoinDelay option. Delay to rejoin after channel kick (irc). Closes #322
## Bugfix
* telegram: Also use HTML in edited messages (telegram). Closes #315
* matrix: Fix panic (matrix). Closes #316
# v1.5.1
## Bugfix
* irc: Fix irc ACTION regression (irc). Closes #306
* irc: Split on UTF-8 for MessageSplit (irc). Closes #308
# v1.5.0
## New features
* general: remote mediaserver support. See MediaServerDownload and MediaServerUpload in matterbridge.toml.sample
more information on https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%5Badvanced%5D
* general: Add support for ReplaceNicks using regexp to replace nicks. Closes #269 (see matterbridge.toml.sample)
* general: Add support for ReplaceMessages using regexp to replace messages. #269 (see matterbridge.toml.sample)
* irc: Add MessageSplit option to split messages on MessageLength (irc). Closes #281
* matrix: Add support for uploading images/video (matrix). Closes #302
* matrix: Add support for uploaded images/video (matrix)
## Bugfix
* telegram: Add webp extension to stickers if necessary (telegram)
* mattermost: Break when re-login fails (mattermost)
# v1.4.1
## Bugfix
* telegram: fix issue with uploading for images/documents/stickers
* slack: remove double messages sent to other bridges when uploading files
* irc: Fix strict user handling of girc (irc). Closes #298
# v1.4.0 # v1.4.0
## Breaking changes ## Breaking changes
* general: `[general]` settings don't override the specific bridge settings * general: `[general]` settings don't override the specific bridge settings

11
docker/arm/Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM cmosh/alpine-arm: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 -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge \
&& rm -rf /go \
&& apk del --purge git go gcc musl-dev

View File

@ -1,13 +1,16 @@
package gateway package gateway
import ( import (
"bytes"
"fmt" "fmt"
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
// "github.com/davecgh/go-spew/spew" // "github.com/davecgh/go-spew/spew"
"crypto/sha1"
"github.com/hashicorp/golang-lru" "github.com/hashicorp/golang-lru"
"github.com/peterhellberg/emojilib" "github.com/peterhellberg/emojilib"
"net/http"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -26,8 +29,15 @@ type Gateway struct {
} }
type BrMsgID struct { type BrMsgID struct {
br *bridge.Bridge br *bridge.Bridge
ID string ID string
ChannelID string
}
var flog *log.Entry
func init() {
flog = log.WithFields(log.Fields{"prefix": "gateway"})
} }
func New(cfg config.Gateway, r *Router) *Gateway { func New(cfg config.Gateway, r *Router) *Gateway {
@ -74,10 +84,10 @@ func (gw *Gateway) reconnectBridge(br *bridge.Bridge) {
br.Disconnect() br.Disconnect()
time.Sleep(time.Second * 5) time.Sleep(time.Second * 5)
RECONNECT: RECONNECT:
log.Infof("Reconnecting %s", br.Account) flog.Infof("Reconnecting %s", br.Account)
err := br.Connect() err := br.Connect()
if err != nil { if err != nil {
log.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err) flog.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err)
time.Sleep(time.Second * 60) time.Sleep(time.Second * 60)
goto RECONNECT goto RECONNECT
} }
@ -90,6 +100,10 @@ func (gw *Gateway) mapChannelConfig(cfg []config.Bridge, direction string) {
if isApi(br.Account) { if isApi(br.Account) {
br.Channel = "api" br.Channel = "api"
} }
// make sure to lowercase irc channels in config #348
if strings.HasPrefix(br.Account, "irc.") {
br.Channel = strings.ToLower(br.Channel)
}
ID := br.Channel + br.Account ID := br.Channel + br.Account
if _, ok := gw.Channels[ID]; !ok { if _, ok := gw.Channels[ID]; !ok {
channel := &config.ChannelInfo{Name: br.Channel, Direction: direction, ID: ID, Options: br.Options, Account: br.Account, channel := &config.ChannelInfo{Name: br.Channel, Direction: direction, ID: ID, Options: br.Options, Account: br.Account,
@ -115,6 +129,12 @@ func (gw *Gateway) mapChannels() error {
func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []config.ChannelInfo { func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []config.ChannelInfo {
var channels []config.ChannelInfo var channels []config.ChannelInfo
// for messages received from the api check that the gateway is the specified one
if msg.Protocol == "api" && gw.Name != msg.Gateway {
return channels
}
// if source channel is in only, do nothing // if source channel is in only, do nothing
for _, channel := range gw.Channels { for _, channel := range gw.Channels {
// lookup the channel from the message // lookup the channel from the message
@ -131,7 +151,7 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
continue continue
} }
// do samechannelgateway logic // do samechannelgateway flogic
if channel.SameChannel[msg.Gateway] { if channel.SameChannel[msg.Gateway] {
if msg.Channel == channel.Name && msg.Account != dest.Account { if msg.Channel == channel.Name && msg.Account != dest.Account {
channels = append(channels, *channel) channels = append(channels, *channel)
@ -152,30 +172,55 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM
// only slack now, check will have to be done in the different bridges. // only slack now, check will have to be done in the different bridges.
// we need to check if we can't use fallback or text in other bridges // we need to check if we can't use fallback or text in other bridges
if msg.Extra != nil { if msg.Extra != nil {
if dest.Protocol != "slack" { if dest.Protocol != "discord" &&
dest.Protocol != "slack" &&
dest.Protocol != "mattermost" &&
dest.Protocol != "telegram" &&
dest.Protocol != "matrix" &&
dest.Protocol != "xmpp" &&
len(msg.Extra[config.EVENT_FILE_FAILURE_SIZE]) == 0 {
if msg.Text == "" { if msg.Text == "" {
return brMsgIDs return brMsgIDs
} }
} }
} }
// Avatar downloads are only relevant for telegram and mattermost for now
if msg.Event == config.EVENT_AVATAR_DOWNLOAD {
if dest.Protocol != "mattermost" &&
dest.Protocol != "telegram" {
return brMsgIDs
}
}
// only relay join/part when configged // only relay join/part when configged
if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].Config.ShowJoinPart { if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].Config.ShowJoinPart {
return brMsgIDs return brMsgIDs
} }
if msg.Event == config.EVENT_TOPIC_CHANGE && !gw.Bridges[dest.Account].Config.ShowTopicChange {
return brMsgIDs
}
// broadcast to every out channel (irc QUIT) // broadcast to every out channel (irc QUIT)
if msg.Channel == "" && msg.Event != config.EVENT_JOIN_LEAVE { if msg.Channel == "" && msg.Event != config.EVENT_JOIN_LEAVE {
log.Debug("empty channel") flog.Debug("empty channel")
return brMsgIDs return brMsgIDs
} }
originchannel := msg.Channel originchannel := msg.Channel
origmsg := msg origmsg := msg
channels := gw.getDestChannel(&msg, *dest) channels := gw.getDestChannel(&msg, *dest)
for _, channel := range channels { for _, channel := range channels {
// do not send to ourself // Only send the avatar download event to ourselves.
if channel.ID == getChannelID(origmsg) { if msg.Event == config.EVENT_AVATAR_DOWNLOAD {
continue if channel.ID != getChannelID(origmsg) {
continue
}
} else {
// do not send to ourself for any other event
if channel.ID == getChannelID(origmsg) {
continue
}
} }
log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel.Name) flog.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel.Name)
msg.Channel = channel.Name msg.Channel = channel.Name
msg.Avatar = gw.modifyAvatar(origmsg, dest) msg.Avatar = gw.modifyAvatar(origmsg, dest)
msg.Username = gw.modifyUsername(origmsg, dest) msg.Username = gw.modifyUsername(origmsg, dest)
@ -183,7 +228,9 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM
if res, ok := gw.Messages.Get(origmsg.ID); ok { if res, ok := gw.Messages.Get(origmsg.ID); ok {
IDs := res.([]*BrMsgID) IDs := res.([]*BrMsgID)
for _, id := range IDs { for _, id := range IDs {
if dest.Protocol == id.br.Protocol { // check protocol, bridge name and channelname
// for people that reuse the same bridge multiple times. see #342
if dest.Protocol == id.br.Protocol && dest.Name == id.br.Name && channel.ID == id.ChannelID {
msg.ID = id.ID msg.ID = id.ID
} }
} }
@ -198,7 +245,7 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM
} }
// append the message ID (mID) from this bridge (dest) to our brMsgIDs slice // append the message ID (mID) from this bridge (dest) to our brMsgIDs slice
if mID != "" { if mID != "" {
brMsgIDs = append(brMsgIDs, &BrMsgID{dest, mID}) brMsgIDs = append(brMsgIDs, &BrMsgID{dest, mID, channel.ID})
} }
} }
return brMsgIDs return brMsgIDs
@ -210,16 +257,19 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
return true return true
} }
if msg.Text == "" { if msg.Text == "" {
// we have an attachment // we have an attachment or actual bytes
if msg.Extra != nil && msg.Extra["attachments"] != nil { if msg.Extra != nil &&
(msg.Extra["attachments"] != nil ||
len(msg.Extra["file"]) > 0 ||
len(msg.Extra[config.EVENT_FILE_FAILURE_SIZE]) > 0) {
return false return false
} }
log.Debugf("ignoring empty message %#v from %s", msg, msg.Account) flog.Debugf("ignoring empty message %#v from %s", msg, msg.Account)
return true return true
} }
for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreNicks) { for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreNicks) {
if msg.Username == entry { if msg.Username == entry {
log.Debugf("ignoring %s from %s", msg.Username, msg.Account) flog.Debugf("ignoring %s from %s", msg.Username, msg.Account)
return true return true
} }
} }
@ -228,11 +278,11 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
if entry != "" { if entry != "" {
re, err := regexp.Compile(entry) re, err := regexp.Compile(entry)
if err != nil { if err != nil {
log.Errorf("incorrect regexp %s for %s", entry, msg.Account) flog.Errorf("incorrect regexp %s for %s", entry, msg.Account)
continue continue
} }
if re.MatchString(msg.Text) { if re.MatchString(msg.Text) {
log.Debugf("matching %s. ignoring %s from %s", entry, msg.Text, msg.Account) flog.Debugf("matching %s. ignoring %s from %s", entry, msg.Text, msg.Account)
return true return true
} }
} }
@ -251,6 +301,20 @@ func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) strin
if nick == "" { if nick == "" {
nick = gw.Config.General.RemoteNickFormat nick = gw.Config.General.RemoteNickFormat
} }
// loop to replace nicks
for _, outer := range br.Config.ReplaceNicks {
search := outer[0]
replace := outer[1]
// TODO move compile to bridge init somewhere
re, err := regexp.Compile(search)
if err != nil {
flog.Errorf("regexp in %s failed: %s", msg.Account, err)
break
}
msg.Username = re.ReplaceAllString(msg.Username, replace)
}
if len(msg.Username) > 0 { if len(msg.Username) > 0 {
// fix utf-8 issue #193 // fix utf-8 issue #193
i := 0 i := 0
@ -263,9 +327,10 @@ func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) strin
} }
nick = strings.Replace(nick, "{NOPINGNICK}", msg.Username[:i]+""+msg.Username[i:], -1) nick = strings.Replace(nick, "{NOPINGNICK}", msg.Username[:i]+""+msg.Username[i:], -1)
} }
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1) nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1)
nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1) nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1)
nick = strings.Replace(nick, "{LABEL}", br.Config.Label, -1)
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
return nick return nick
} }
@ -284,7 +349,55 @@ func (gw *Gateway) modifyAvatar(msg config.Message, dest *bridge.Bridge) string
func (gw *Gateway) modifyMessage(msg *config.Message) { func (gw *Gateway) modifyMessage(msg *config.Message) {
// replace :emoji: to unicode // replace :emoji: to unicode
msg.Text = emojilib.Replace(msg.Text) msg.Text = emojilib.Replace(msg.Text)
msg.Gateway = gw.Name br := gw.Bridges[msg.Account]
// loop to replace messages
for _, outer := range br.Config.ReplaceMessages {
search := outer[0]
replace := outer[1]
// TODO move compile to bridge init somewhere
re, err := regexp.Compile(search)
if err != nil {
flog.Errorf("regexp in %s failed: %s", msg.Account, err)
break
}
msg.Text = re.ReplaceAllString(msg.Text, replace)
}
// messages from api have Gateway specified, don't overwrite
if msg.Protocol != "api" {
msg.Gateway = gw.Name
}
}
func (gw *Gateway) handleFiles(msg *config.Message) {
if msg.Extra == nil || gw.Config.General.MediaServerUpload == "" {
return
}
if len(msg.Extra["file"]) > 0 {
client := &http.Client{
Timeout: time.Second * 5,
}
for i, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))
reader := bytes.NewReader(*fi.Data)
url := gw.Config.General.MediaServerUpload + "/" + sha1sum + "/" + fi.Name
durl := gw.Config.General.MediaServerDownload + "/" + sha1sum + "/" + fi.Name
extra := msg.Extra["file"][i].(config.FileInfo)
extra.URL = durl
req, _ := http.NewRequest("PUT", url, reader)
req.Header.Set("Content-Type", "binary/octet-stream")
_, err := client.Do(req)
if err != nil {
flog.Errorf("mediaserver upload failed: %#v", err)
continue
}
flog.Debugf("mediaserver download URL = %s", durl)
// we uploaded the file successfully. Add the SHA
extra.SHA = sha1sum
msg.Extra["file"][i] = extra
}
}
} }
func getChannelID(msg config.Message) string { func getChannelID(msg config.Message) string {

View File

@ -5,7 +5,7 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway/samechannel" "github.com/42wim/matterbridge/gateway/samechannel"
log "github.com/Sirupsen/logrus" //log "github.com/sirupsen/logrus"
// "github.com/davecgh/go-spew/spew" // "github.com/davecgh/go-spew/spew"
"time" "time"
) )
@ -42,12 +42,13 @@ func NewRouter(cfg *config.Config) (*Router, error) {
func (r *Router) Start() error { func (r *Router) Start() error {
m := make(map[string]*bridge.Bridge) m := make(map[string]*bridge.Bridge)
for _, gw := range r.Gateways { for _, gw := range r.Gateways {
flog.Infof("Parsing gateway %s", gw.Name)
for _, br := range gw.Bridges { for _, br := range gw.Bridges {
m[br.Account] = br m[br.Account] = br
} }
} }
for _, br := range m { for _, br := range m {
log.Infof("Starting bridge: %s ", br.Account) flog.Infof("Starting bridge: %s ", br.Account)
err := br.Connect() err := br.Connect()
if err != nil { if err != nil {
return fmt.Errorf("Bridge %s failed to start: %v", br.Account, err) return fmt.Errorf("Bridge %s failed to start: %v", br.Account, err)
@ -99,6 +100,7 @@ func (r *Router) handleReceive() {
if !gw.ignoreMessage(&msg) { if !gw.ignoreMessage(&msg) {
msg.Timestamp = time.Now() msg.Timestamp = time.Now()
gw.modifyMessage(&msg) gw.modifyMessage(&msg)
gw.handleFiles(&msg)
for _, br := range gw.Bridges { for _, br := range gw.Bridges {
msgIDs = append(msgIDs, gw.handleMessage(msg, br)...) msgIDs = append(msgIDs, gw.handleMessage(msg, br)...)
} }

View File

@ -5,22 +5,21 @@ import (
"fmt" "fmt"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway" "github.com/42wim/matterbridge/gateway"
log "github.com/Sirupsen/logrus"
"github.com/google/gops/agent" "github.com/google/gops/agent"
log "github.com/sirupsen/logrus"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
"os" "os"
"strings" "strings"
) )
var ( var (
version = "1.4.0" version = "1.8.0"
githash string githash string
) )
func init() {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
}
func main() { func main() {
log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: true})
flog := log.WithFields(log.Fields{"prefix": "main"})
flagConfig := flag.String("conf", "matterbridge.toml", "config file") flagConfig := flag.String("conf", "matterbridge.toml", "config file")
flagDebug := flag.Bool("debug", false, "enable debug") flagDebug := flag.Bool("debug", false, "enable debug")
flagVersion := flag.Bool("version", false, "show version") flagVersion := flag.Bool("version", false, "show version")
@ -35,22 +34,24 @@ func main() {
return return
} }
if *flagDebug || os.Getenv("DEBUG") == "1" { if *flagDebug || os.Getenv("DEBUG") == "1" {
log.Info("Enabling debug") log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: false})
flog.Info("Enabling debug")
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
} }
log.Printf("Running version %s %s", version, githash) flog.Printf("Running version %s %s", version, githash)
if strings.Contains(version, "-dev") { if strings.Contains(version, "-dev") {
log.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.") flog.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
} }
cfg := config.NewConfig(*flagConfig) cfg := config.NewConfig(*flagConfig)
cfg.General.Debug = *flagDebug
r, err := gateway.NewRouter(cfg) r, err := gateway.NewRouter(cfg)
if err != nil { if err != nil {
log.Fatalf("Starting gateway failed: %s", err) flog.Fatalf("Starting gateway failed: %s", err)
} }
err = r.Start() err = r.Start()
if err != nil { if err != nil {
log.Fatalf("Starting gateway failed: %s", err) flog.Fatalf("Starting gateway failed: %s", err)
} }
log.Printf("Gateway(s) started succesfully. Now relaying messages") flog.Printf("Gateway(s) started succesfully. Now relaying messages")
select {} select {}
} }

View File

@ -80,6 +80,15 @@ MessageQueue=30
#OPTIONAL (default 400) #OPTIONAL (default 400)
MessageLength=400 MessageLength=400
#Split messages on MessageLength instead of showing the <message clipped>
#WARNING: this could lead to flooding
#OPTIONAL (default false)
MessageSplit=false
#Delay in seconds to rejoin a channel when kicked
#OPTIONAL (default 0)
RejoinDelay=0
#Nicks you want to ignore. #Nicks you want to ignore.
#Messages from those users will not be sent to other bridges. #Messages from those users will not be sent to other bridges.
#OPTIONAL #OPTIONAL
@ -91,16 +100,38 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged. See https://github.com/42wim/matterbridge/issues/175 for more information #The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged. See https://github.com/42wim/matterbridge/issues/175 for more information
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -109,6 +140,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=false StripNick=false
#Enable to show topic changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
################################################################### ###################################################################
#XMPP section #XMPP section
################################################################### ###################################################################
@ -154,15 +190,37 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#Messages you want to replace.
#It replaces outgoing messages from the bridge.
#So you need to place it by the sending bridge definition.
#Regular expressions supported
#Some examples:
#This replaces cat => dog and sleep => awake
#ReplaceMessages=[ ["cat","dog"], ["sleep","awake"] ]
#This Replaces every number with number. 123 => numbernumbernumber
#ReplaceMessages=[ ["[0-9]","number"] ]
#OPTIONAL (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#Nicks you want to replace.
#See ReplaceMessages for syntaxA
#OPTIONAL (default empty)
ReplaceNicks=[ ["user--","user"] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -171,6 +229,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=false StripNick=false
#Enable to show topic changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
################################################################### ###################################################################
#hipchat section #hipchat section
################################################################### ###################################################################
@ -208,15 +271,37 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -225,6 +310,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=false StripNick=false
#Enable to show topic changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
################################################################### ###################################################################
#mattermost section #mattermost section
################################################################### ###################################################################
@ -322,15 +412,37 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -339,6 +451,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=false StripNick=false
#Enable to show topic changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
################################################################### ###################################################################
#Gitter section #Gitter section
#Best to make a dedicated gitter account for the bot. #Best to make a dedicated gitter account for the bot.
@ -366,15 +483,37 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -383,6 +522,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=false StripNick=false
#Enable to show topic changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
################################################################### ###################################################################
#slack section #slack section
################################################################### ###################################################################
@ -418,6 +562,7 @@ WebhookBindAddress="0.0.0.0:9999"
#Icon that will be showed in slack #Icon that will be showed in slack
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL #OPTIONAL
IconURL="https://robohash.org/{NICK}.png?size=48x48" IconURL="https://robohash.org/{NICK}.png?size=48x48"
@ -457,15 +602,37 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -474,6 +641,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=false StripNick=false
#Enable to show topic changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
################################################################### ###################################################################
#discord section #discord section
################################################################### ###################################################################
@ -525,15 +697,37 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -542,6 +736,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=false StripNick=false
#Enable to show topic changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
################################################################### ###################################################################
#telegram section #telegram section
################################################################### ###################################################################
@ -592,15 +791,37 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -609,6 +830,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=false StripNick=false
#Enable to show topic changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
################################################################### ###################################################################
#rocketchat section #rocketchat section
################################################################### ###################################################################
@ -660,15 +886,37 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -677,6 +925,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=false StripNick=false
#Enable to show topic changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
################################################################### ###################################################################
#matrix section #matrix section
################################################################### ###################################################################
@ -720,15 +973,37 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -737,6 +1012,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=false StripNick=false
#Enable to show topic changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
################################################################### ###################################################################
#steam section #steam section
################################################################### ###################################################################
@ -774,15 +1054,37 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -791,6 +1093,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=false StripNick=false
#Enable to show topic changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
################################################################### ###################################################################
#API #API
################################################################### ###################################################################
@ -812,9 +1119,14 @@ Buffer=1000
#OPTIONAL (no authorization if token is empty) #OPTIONAL (no authorization if token is empty)
Token="mytoken" Token="mytoken"
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="{NICK}" RemoteNickFormat="{NICK}"
@ -829,6 +1141,7 @@ RemoteNickFormat="{NICK}"
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
@ -838,6 +1151,30 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=false StripNick=false
#MediaServerUpload and MediaServerDownload are used for uploading images/files/video to
#a remote "mediaserver" (a webserver like caddy for example).
#When configured images/files uploaded on bridges like mattermost,slack, telegram will be downloaded
#and uploaded again to MediaServerUpload URL
#The MediaServerDownload will be used so that bridges without native uploading support:
#gitter, irc and xmpp will be shown links to the files on MediaServerDownload
#
#More information https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%5Badvanced%5D
#OPTIONAL (default empty)
MediaServerUpload="https://user:pass@yourserver.com/upload"
#OPTIONAL (default empty)
MediaServerDownload="https://youserver.com/download"
#MediaDownloadSize is the maximum size of attachments, videos, images
#matterbridge will download and upload this file to bridges that also support uploading files.
#eg downloading from slack to upload it to mattermost
#
#It will only download from bridges that don't have public links available, which are for the moment
#slack, telegram, matrix and mattermost
#
#Optional (default 1000000 (1 megabyte))
MediaDownloadSize=1000000
################################################################### ###################################################################
#Gateway configuration #Gateway configuration
################################################################### ###################################################################

View File

@ -13,7 +13,8 @@ import (
"sync" "sync"
"time" "time"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/hashicorp/golang-lru" "github.com/hashicorp/golang-lru"
@ -73,12 +74,16 @@ type MMClient struct {
func New(login, pass, team, server string) *MMClient { func New(login, pass, team, server string) *MMClient {
cred := &Credentials{Login: login, Pass: pass, Team: team, Server: server} 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 := &MMClient{Credentials: cred, MessageChan: make(chan *Message, 100), Users: make(map[string]*model.User)}
mmclient.log = log.WithFields(log.Fields{"module": "matterclient"}) log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true})
log.SetFormatter(&log.TextFormatter{FullTimestamp: true}) mmclient.log = log.WithFields(log.Fields{"prefix": "matterclient"})
mmclient.lruCache, _ = lru.New(500) mmclient.lruCache, _ = lru.New(500)
return mmclient return mmclient
} }
func (m *MMClient) SetDebugLog() {
log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: false})
}
func (m *MMClient) SetLogLevel(level string) { func (m *MMClient) SetLogLevel(level string) {
l, err := log.ParseLevel(level) l, err := log.ParseLevel(level)
if err != nil { if err != nil {
@ -585,9 +590,9 @@ func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
func (m *MMClient) UpdateLastViewed(channelId string) { func (m *MMClient) UpdateLastViewed(channelId string) {
m.log.Debugf("posting lastview %#v", channelId) m.log.Debugf("posting lastview %#v", channelId)
view := &model.ChannelView{ChannelId: channelId} view := &model.ChannelView{ChannelId: channelId}
res, _ := m.Client.ViewChannel(m.User.Id, view) _, resp := m.Client.ViewChannel(m.User.Id, view)
if !res { if resp.Error != nil {
m.log.Errorf("ChannelView update for %s failed", channelId) m.log.Errorf("ChannelView update for %s failed: %s", channelId, resp.Error)
} }
} }
@ -817,9 +822,14 @@ func (m *MMClient) StatusLoop() {
backoff = time.Second * 60 backoff = time.Second * 60
case <-time.After(time.Second * 5): case <-time.After(time.Second * 5):
if retries > 3 { if retries > 3 {
m.log.Debug("StatusLoop() timeout")
m.Logout() m.Logout()
m.WsQuit = false m.WsQuit = false
m.Login() err := m.Login()
if err != nil {
log.Errorf("Login failed: %#v", err)
break
}
if m.OnWsConnect != nil { if m.OnWsConnect != nil {
m.OnWsConnect() m.OnWsConnect()
} }

View File

@ -1,30 +0,0 @@
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!")
}

View File

@ -1,67 +0,0 @@
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)
}

View File

@ -1,10 +0,0 @@
// +build appengine
package logrus
import "io"
// IsTerminal returns true if stderr's file descriptor is a terminal.
func IsTerminal(f io.Writer) bool {
return true
}

View File

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

View File

@ -1,28 +0,0 @@
// 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
// +build !appengine
package logrus
import (
"io"
"os"
"syscall"
"unsafe"
)
// IsTerminal returns true if stderr's file descriptor is a terminal.
func IsTerminal(f io.Writer) bool {
var termios Termios
switch v := f.(type) {
case *os.File:
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(v.Fd()), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
default:
return false
}
}

View File

@ -1,21 +0,0 @@
// +build solaris,!appengine
package logrus
import (
"io"
"os"
"golang.org/x/sys/unix"
)
// IsTerminal returns true if the given file descriptor is a terminal.
func IsTerminal(f io.Writer) bool {
switch v := f.(type) {
case *os.File:
_, err := unix.IoctlGetTermios(int(v.Fd()), unix.TCGETA)
return err == nil
default:
return false
}
}

View File

@ -1,33 +0,0 @@
// 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,!appengine
package logrus
import (
"io"
"os"
"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(f io.Writer) bool {
switch v := f.(type) {
case *os.File:
var st uint32
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(v.Fd()), uintptr(unsafe.Pointer(&st)), 0)
return r != 0 && e == 0
default:
return false
}
}

View File

@ -21,7 +21,7 @@ import (
) )
// VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
const VERSION = "0.17.0" const VERSION = "0.18.0"
// ErrMFA will be risen by New when the user has 2FA. // ErrMFA will be risen by New when the user has 2FA.
var ErrMFA = errors.New("account has 2FA enabled") var ErrMFA = errors.New("account has 2FA enabled")
@ -50,7 +50,7 @@ func New(args ...interface{}) (s *Session, err error) {
// Create an empty Session interface. // Create an empty Session interface.
s = &Session{ s = &Session{
State: NewState(), State: NewState(),
ratelimiter: NewRatelimiter(), Ratelimiter: NewRatelimiter(),
StateEnabled: true, StateEnabled: true,
Compress: true, Compress: true,
ShouldReconnectOnError: true, ShouldReconnectOnError: true,

View File

@ -71,7 +71,6 @@ var (
EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID } EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID }
EndpointGuild = func(gID string) string { return EndpointGuilds + gID } 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" } EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" }
EndpointGuildMembers = func(gID string) string { return EndpointGuilds + gID + "/members" } EndpointGuildMembers = func(gID string) string { return EndpointGuilds + gID + "/members" }
EndpointGuildMember = func(gID, uID string) string { return EndpointGuilds + gID + "/members/" + uID } EndpointGuildMember = func(gID, uID string) string { return EndpointGuilds + gID + "/members/" + uID }
@ -98,7 +97,7 @@ var (
EndpointChannelMessages = func(cID string) string { return EndpointChannels + cID + "/messages" } EndpointChannelMessages = func(cID string) string { return EndpointChannels + cID + "/messages" }
EndpointChannelMessage = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID } EndpointChannelMessage = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID }
EndpointChannelMessageAck = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID + "/ack" } EndpointChannelMessageAck = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID + "/ack" }
EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk_delete" } EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk-delete" }
EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" } EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" }
EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID } EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID }
@ -122,6 +121,8 @@ var (
EndpointRelationship = func(uID string) string { return EndpointRelationships() + "/" + uID } EndpointRelationship = func(uID string) string { return EndpointRelationships() + "/" + uID }
EndpointRelationshipsMutual = func(uID string) string { return EndpointUsers + uID + "/relationships" } EndpointRelationshipsMutual = func(uID string) string { return EndpointUsers + uID + "/relationships" }
EndpointGuildCreate = EndpointAPI + "guilds"
EndpointInvite = func(iID string) string { return EndpointAPI + "invite/" + iID } EndpointInvite = func(iID string) string { return EndpointAPI + "invite/" + iID }
EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" } EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" }

View File

@ -6,7 +6,7 @@ type EventHandler interface {
Type() string Type() string
// Handle is called whenever an event of Type() happens. // Handle is called whenever an event of Type() happens.
// It is the recievers responsibility to type assert that the interface // It is the receivers responsibility to type assert that the interface
// is the expected struct. // is the expected struct.
Handle(*Session, interface{}) Handle(*Session, interface{})
} }

View File

@ -79,7 +79,7 @@ func main() {
ap.Name = Name ap.Name = Name
ap, err = dg.ApplicationCreate(ap) ap, err = dg.ApplicationCreate(ap)
if err != nil { if err != nil {
fmt.Println("error creating new applicaiton,", err) fmt.Println("error creating new application,", err)
return return
} }

View File

@ -23,7 +23,7 @@ const (
LogError int = iota LogError int = iota
// LogWarning level is used for very abnormal events and errors that are // LogWarning level is used for very abnormal events and errors that are
// also returend to a calling function. // also returned to a calling function.
LogWarning LogWarning
// LogInformational level is used for normal non-error activity // LogInformational level is used for normal non-error activity
@ -34,26 +34,34 @@ const (
LogDebug LogDebug
) )
// Logger can be used to replace the standard logging for discordgo
var Logger func(msgL, caller int, format string, a ...interface{})
// msglog provides package wide logging consistancy for discordgo // msglog provides package wide logging consistancy for discordgo
// the format, a... portion this command follows that of fmt.Printf // the format, a... portion this command follows that of fmt.Printf
// msgL : LogLevel of the message // msgL : LogLevel of the message
// caller : 1 + the number of callers away from the message source // caller : 1 + the number of callers away from the message source
// format : Printf style message format // format : Printf style message format
// a ... : comma seperated list of values to pass // a ... : comma separated list of values to pass
func msglog(msgL, caller int, format string, a ...interface{}) { func msglog(msgL, caller int, format string, a ...interface{}) {
pc, file, line, _ := runtime.Caller(caller) if Logger != nil {
Logger(msgL, caller, format, a...)
} else {
files := strings.Split(file, "/") pc, file, line, _ := runtime.Caller(caller)
file = files[len(files)-1]
name := runtime.FuncForPC(pc).Name() files := strings.Split(file, "/")
fns := strings.Split(name, ".") file = files[len(files)-1]
name = fns[len(fns)-1]
msg := fmt.Sprintf(format, a...) name := runtime.FuncForPC(pc).Name()
fns := strings.Split(name, ".")
name = fns[len(fns)-1]
log.Printf("[DG%d] %s:%d:%s() %s\n", msgL, file, line, name, msg) 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 // helper function that wraps msglog for the Session struct

View File

@ -237,7 +237,7 @@ func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, e
continue continue
} }
content = strings.Replace(content, "<&"+role.ID+">", "@"+role.Name, -1) content = strings.Replace(content, "<@&"+role.ID+">", "@"+role.Name, -1)
} }
content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string { content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string {

View File

@ -41,8 +41,8 @@ func NewRatelimiter() *RateLimiter {
} }
} }
// getBucket retrieves or creates a bucket // GetBucket retrieves or creates a bucket
func (r *RateLimiter) getBucket(key string) *Bucket { func (r *RateLimiter) GetBucket(key string) *Bucket {
r.Lock() r.Lock()
defer r.Unlock() defer r.Unlock()
@ -51,7 +51,7 @@ func (r *RateLimiter) getBucket(key string) *Bucket {
} }
b := &Bucket{ b := &Bucket{
remaining: 1, Remaining: 1,
Key: key, Key: key,
global: r.global, global: r.global,
} }
@ -68,27 +68,37 @@ func (r *RateLimiter) getBucket(key string) *Bucket {
return b return b
} }
// LockBucket Locks until a request can be made // GetWaitTime returns the duration you should wait for a Bucket
func (r *RateLimiter) LockBucket(bucketID string) *Bucket { func (r *RateLimiter) GetWaitTime(b *Bucket, minRemaining int) time.Duration {
b := r.getBucket(bucketID)
b.Lock()
// If we ran out of calls and the reset time is still ahead of us // If we ran out of calls and the reset time is still ahead of us
// then we need to take it easy and relax a little // then we need to take it easy and relax a little
if b.remaining < 1 && b.reset.After(time.Now()) { if b.Remaining < minRemaining && b.reset.After(time.Now()) {
time.Sleep(b.reset.Sub(time.Now())) return b.reset.Sub(time.Now())
} }
// Check for global ratelimits // Check for global ratelimits
sleepTo := time.Unix(0, atomic.LoadInt64(r.global)) sleepTo := time.Unix(0, atomic.LoadInt64(r.global))
if now := time.Now(); now.Before(sleepTo) { if now := time.Now(); now.Before(sleepTo) {
time.Sleep(sleepTo.Sub(now)) return sleepTo.Sub(now)
} }
b.remaining-- return 0
}
// LockBucket Locks until a request can be made
func (r *RateLimiter) LockBucket(bucketID string) *Bucket {
return r.LockBucketObject(r.GetBucket(bucketID))
}
// LockBucketObject Locks an already resolved bucket until a request can be made
func (r *RateLimiter) LockBucketObject(b *Bucket) *Bucket {
b.Lock()
if wait := r.GetWaitTime(b, 1); wait > 0 {
time.Sleep(wait)
}
b.Remaining--
return b return b
} }
@ -96,13 +106,14 @@ func (r *RateLimiter) LockBucket(bucketID string) *Bucket {
type Bucket struct { type Bucket struct {
sync.Mutex sync.Mutex
Key string Key string
remaining int Remaining int
limit int limit int
reset time.Time reset time.Time
global *int64 global *int64
lastReset time.Time lastReset time.Time
customRateLimit *customRateLimit customRateLimit *customRateLimit
Userdata interface{}
} }
// Release unlocks the bucket and reads the headers to update the buckets ratelimit info // Release unlocks the bucket and reads the headers to update the buckets ratelimit info
@ -113,10 +124,10 @@ func (b *Bucket) Release(headers http.Header) error {
// Check if the bucket uses a custom ratelimiter // Check if the bucket uses a custom ratelimiter
if rl := b.customRateLimit; rl != nil { if rl := b.customRateLimit; rl != nil {
if time.Now().Sub(b.lastReset) >= rl.reset { if time.Now().Sub(b.lastReset) >= rl.reset {
b.remaining = rl.requests - 1 b.Remaining = rl.requests - 1
b.lastReset = time.Now() b.lastReset = time.Now()
} }
if b.remaining < 1 { if b.Remaining < 1 {
b.reset = time.Now().Add(rl.reset) b.reset = time.Now().Add(rl.reset)
} }
return nil return nil
@ -176,7 +187,7 @@ func (b *Bucket) Release(headers http.Header) error {
if err != nil { if err != nil {
return err return err
} }
b.remaining = int(parsedRemaining) b.Remaining = int(parsedRemaining)
} }
return nil return nil

View File

@ -65,9 +65,11 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID
if bucketID == "" { if bucketID == "" {
bucketID = strings.SplitN(urlStr, "?", 2)[0] bucketID = strings.SplitN(urlStr, "?", 2)[0]
} }
return s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucket(bucketID), sequence)
}
bucket := s.ratelimiter.LockBucket(bucketID) // RequestWithLockedBucket makes a request using a bucket that's already been locked
func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b []byte, bucket *Bucket, sequence int) (response []byte, err error) {
if s.Debug { if s.Debug {
log.Printf("API REQUEST %8s :: %s\n", method, urlStr) log.Printf("API REQUEST %8s :: %s\n", method, urlStr)
log.Printf("API REQUEST PAYLOAD :: [%s]\n", string(b)) log.Printf("API REQUEST PAYLOAD :: [%s]\n", string(b))
@ -139,7 +141,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID
if sequence < s.MaxRestRetries { if sequence < s.MaxRestRetries {
s.log(LogInformational, "%s Failed (%s), Retrying...", urlStr, resp.Status) s.log(LogInformational, "%s Failed (%s), Retrying...", urlStr, resp.Status)
response, err = s.request(method, urlStr, contentType, b, bucketID, sequence+1) response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence+1)
} else { } else {
err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response) err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response)
} }
@ -158,7 +160,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID
// we can make the above smarter // we can make the above smarter
// this method can cause longer delays than required // this method can cause longer delays than required
response, err = s.request(method, urlStr, contentType, b, bucketID, sequence) response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence)
default: // Error condition default: // Error condition
err = newRestError(req, resp, response) err = newRestError(req, resp, response)
@ -585,7 +587,7 @@ func (s *Session) GuildCreate(name string) (st *Guild, err error) {
Name string `json:"name"` Name string `json:"name"`
}{name} }{name}
body, err := s.RequestWithBucketID("POST", EndpointGuilds, data, EndpointGuilds) body, err := s.RequestWithBucketID("POST", EndpointGuildCreate, data, EndpointGuildCreate)
if err != nil { if err != nil {
return return
} }
@ -907,7 +909,7 @@ func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err
// GuildInvites returns an array of Invite structures for the given guild // GuildInvites returns an array of Invite structures for the given guild
// guildID : The ID of a Guild. // guildID : The ID of a Guild.
func (s *Session) GuildInvites(guildID string) (st []*Invite, err error) { func (s *Session) GuildInvites(guildID string) (st []*Invite, err error) {
body, err := s.RequestWithBucketID("GET", EndpointGuildInvites(guildID), nil, EndpointGuildInivtes(guildID)) body, err := s.RequestWithBucketID("GET", EndpointGuildInvites(guildID), nil, EndpointGuildInvites(guildID))
if err != nil { if err != nil {
return return
} }
@ -957,6 +959,7 @@ func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist b
// Prevent sending a color int that is too big. // Prevent sending a color int that is too big.
if color > 0xFFFFFF { if color > 0xFFFFFF {
err = fmt.Errorf("color value cannot be larger than 0xFFFFFF") err = fmt.Errorf("color value cannot be larger than 0xFFFFFF")
return nil, err
} }
data := struct { data := struct {
@ -1020,6 +1023,9 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er
uri := EndpointGuildPrune(guildID) + fmt.Sprintf("?days=%d", days) uri := EndpointGuildPrune(guildID) + fmt.Sprintf("?days=%d", days)
body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID)) body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID))
if err != nil {
return
}
err = unmarshal(body, &p) err = unmarshal(body, &p)
if err != nil { if err != nil {
@ -1204,7 +1210,7 @@ func (s *Session) GuildEmbedEdit(guildID string, enabled bool, channelID string)
// Functions specific to Discord Channels // Functions specific to Discord Channels
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
// Channel returns a Channel strucutre of a specific Channel. // Channel returns a Channel structure of a specific Channel.
// channelID : The ID of the Channel you want returned. // channelID : The ID of the Channel you want returned.
func (s *Session) Channel(channelID string) (st *Channel, err error) { func (s *Session) Channel(channelID string) (st *Channel, err error) {
body, err := s.RequestWithBucketID("GET", EndpointChannel(channelID), nil, EndpointChannel(channelID)) body, err := s.RequestWithBucketID("GET", EndpointChannel(channelID), nil, EndpointChannel(channelID))
@ -1219,12 +1225,16 @@ func (s *Session) Channel(channelID string) (st *Channel, err error) {
// ChannelEdit edits the given channel // ChannelEdit edits the given channel
// channelID : The ID of a Channel // channelID : The ID of a Channel
// name : The new name to assign the channel. // name : The new name to assign the channel.
func (s *Session) ChannelEdit(channelID, name string) (st *Channel, err error) { func (s *Session) ChannelEdit(channelID, name string) (*Channel, error) {
return s.ChannelEditComplex(channelID, &ChannelEdit{
data := struct { Name: name,
Name string `json:"name"` })
}{name} }
// ChannelEditComplex edits an existing channel, replacing the parameters entirely with ChannelEdit struct
// channelID : The ID of a Channel
// data : The channel struct to send
func (s *Session) ChannelEditComplex(channelID string, data *ChannelEdit) (st *Channel, err error) {
body, err := s.RequestWithBucketID("PATCH", EndpointChannel(channelID), data, EndpointChannel(channelID)) body, err := s.RequestWithBucketID("PATCH", EndpointChannel(channelID), data, EndpointChannel(channelID))
if err != nil { if err != nil {
return return
@ -1476,7 +1486,7 @@ func (s *Session) ChannelMessageDelete(channelID, messageID string) (err error)
} }
// ChannelMessagesBulkDelete bulk deletes the messages from the channel for the provided messageIDs. // ChannelMessagesBulkDelete bulk deletes the messages from the channel for the provided messageIDs.
// If only one messageID is in the slice call channelMessageDelete funciton. // If only one messageID is in the slice call channelMessageDelete function.
// If the slice is empty do nothing. // If the slice is empty do nothing.
// channelID : The ID of the channel for the messages to delete. // channelID : The ID of the channel for the messages to delete.
// messages : The IDs of the messages to be deleted. A slice of string IDs. A maximum of 100 messages. // messages : The IDs of the messages to be deleted. A slice of string IDs. A maximum of 100 messages.
@ -1569,16 +1579,14 @@ func (s *Session) ChannelInvites(channelID string) (st []*Invite, err error) {
// ChannelInviteCreate creates a new invite for the given channel. // ChannelInviteCreate creates a new invite for the given channel.
// channelID : The ID of a Channel // channelID : The ID of a Channel
// i : An Invite struct with the values MaxAge, MaxUses, Temporary, // i : An Invite struct with the values MaxAge, MaxUses and Temporary defined.
// and XkcdPass defined.
func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, err error) { func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, err error) {
data := struct { data := struct {
MaxAge int `json:"max_age"` MaxAge int `json:"max_age"`
MaxUses int `json:"max_uses"` MaxUses int `json:"max_uses"`
Temporary bool `json:"temporary"` Temporary bool `json:"temporary"`
XKCDPass string `json:"xkcdpass"` }{i.MaxAge, i.MaxUses, i.Temporary}
}{i.MaxAge, i.MaxUses, i.Temporary, i.XkcdPass}
body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID)) body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID))
if err != nil { if err != nil {
@ -1618,7 +1626,7 @@ func (s *Session) ChannelPermissionDelete(channelID, targetID string) (err error
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
// Invite returns an Invite structure of the given invite // Invite returns an Invite structure of the given invite
// inviteID : The invite code (or maybe xkcdpass?) // inviteID : The invite code
func (s *Session) Invite(inviteID string) (st *Invite, err error) { func (s *Session) Invite(inviteID string) (st *Invite, err error) {
body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID), nil, EndpointInvite("")) body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID), nil, EndpointInvite(""))
@ -1631,7 +1639,7 @@ func (s *Session) Invite(inviteID string) (st *Invite, err error) {
} }
// InviteDelete deletes an existing invite // InviteDelete deletes an existing invite
// inviteID : the code (or maybe xkcdpass?) of an invite // inviteID : the code of an invite
func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) { func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) {
body, err := s.RequestWithBucketID("DELETE", EndpointInvite(inviteID), nil, EndpointInvite("")) body, err := s.RequestWithBucketID("DELETE", EndpointInvite(inviteID), nil, EndpointInvite(""))
@ -1644,7 +1652,7 @@ func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) {
} }
// InviteAccept accepts an Invite to a Guild or Channel // InviteAccept accepts an Invite to a Guild or Channel
// inviteID : The invite code (or maybe xkcdpass?) // inviteID : The invite code
func (s *Session) InviteAccept(inviteID string) (st *Invite, err error) { func (s *Session) InviteAccept(inviteID string) (st *Invite, err error) {
body, err := s.RequestWithBucketID("POST", EndpointInvite(inviteID), nil, EndpointInvite("")) body, err := s.RequestWithBucketID("POST", EndpointInvite(inviteID), nil, EndpointInvite(""))

View File

@ -531,7 +531,7 @@ func (s *State) PrivateChannel(channelID string) (*Channel, error) {
return s.Channel(channelID) return s.Channel(channelID)
} }
// Channel gets a channel by ID, it will look in all guilds an private channels. // Channel gets a channel by ID, it will look in all guilds and private channels.
func (s *State) Channel(channelID string) (*Channel, error) { func (s *State) Channel(channelID string) (*Channel, error) {
if s == nil { if s == nil {
return nil, ErrNilState return nil, ErrNilState
@ -816,6 +816,13 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
if s.TrackMembers { if s.TrackMembers {
err = s.MemberRemove(t.Member) err = s.MemberRemove(t.Member)
} }
case *GuildMembersChunk:
if s.TrackMembers {
for i := range t.Members {
t.Members[i].GuildID = t.GuildID
err = s.MemberAdd(t.Members[i])
}
}
case *GuildRoleCreate: case *GuildRoleCreate:
if s.TrackRoles { if s.TrackRoles {
err = s.RoleAdd(t.GuildID, t.Role) err = s.RoleAdd(t.GuildID, t.Role)

View File

@ -14,7 +14,6 @@ package discordgo
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv"
"sync" "sync"
"time" "time"
@ -85,6 +84,9 @@ type Session struct {
// Stores the last HeartbeatAck that was recieved (in UTC) // Stores the last HeartbeatAck that was recieved (in UTC)
LastHeartbeatAck time.Time LastHeartbeatAck time.Time
// used to deal with rate limits
Ratelimiter *RateLimiter
// Event handlers // Event handlers
handlersMu sync.RWMutex handlersMu sync.RWMutex
handlers map[string][]*eventHandlerInstance handlers map[string][]*eventHandlerInstance
@ -96,9 +98,6 @@ type Session struct {
// When nil, the session is not listening. // When nil, the session is not listening.
listening chan interface{} listening chan interface{}
// used to deal with rate limits
ratelimiter *RateLimiter
// sequence tracks the current gateway api websocket sequence number // sequence tracks the current gateway api websocket sequence number
sequence *int64 sequence *int64
@ -143,9 +142,9 @@ type Invite struct {
MaxAge int `json:"max_age"` MaxAge int `json:"max_age"`
Uses int `json:"uses"` Uses int `json:"uses"`
MaxUses int `json:"max_uses"` MaxUses int `json:"max_uses"`
XkcdPass string `json:"xkcdpass"`
Revoked bool `json:"revoked"` Revoked bool `json:"revoked"`
Temporary bool `json:"temporary"` Temporary bool `json:"temporary"`
Unique bool `json:"unique"`
} }
// ChannelType is the type of a Channel // ChannelType is the type of a Channel
@ -171,9 +170,22 @@ type Channel struct {
NSFW bool `json:"nsfw"` NSFW bool `json:"nsfw"`
Position int `json:"position"` Position int `json:"position"`
Bitrate int `json:"bitrate"` Bitrate int `json:"bitrate"`
Recipients []*User `json:"recipient"` Recipients []*User `json:"recipients"`
Messages []*Message `json:"-"` Messages []*Message `json:"-"`
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"`
ParentID string `json:"parent_id"`
}
// A ChannelEdit holds Channel Feild data for a channel edit.
type ChannelEdit struct {
Name string `json:"name,omitempty"`
Topic string `json:"topic,omitempty"`
NSFW bool `json:"nsfw,omitempty"`
Position int `json:"position"`
Bitrate int `json:"bitrate,omitempty"`
UserLimit int `json:"user_limit,omitempty"`
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"`
ParentID string `json:"parent_id,omitempty"`
} }
// A PermissionOverwrite holds permission overwrite data for a Channel // A PermissionOverwrite holds permission overwrite data for a Channel
@ -191,6 +203,7 @@ type Emoji struct {
Roles []string `json:"roles"` Roles []string `json:"roles"`
Managed bool `json:"managed"` Managed bool `json:"managed"`
RequireColons bool `json:"require_colons"` RequireColons bool `json:"require_colons"`
Animated bool `json:"animated"`
} }
// APIName returns an correctly formatted API name for use in the MessageReactions endpoints. // APIName returns an correctly formatted API name for use in the MessageReactions endpoints.
@ -204,7 +217,7 @@ func (e *Emoji) APIName() string {
return e.ID return e.ID
} }
// VerificationLevel type defination // VerificationLevel type definition
type VerificationLevel int type VerificationLevel int
// Constants for VerificationLevel levels from 0 to 3 inclusive // Constants for VerificationLevel levels from 0 to 3 inclusive
@ -314,45 +327,58 @@ type Presence struct {
Since *int `json:"since"` Since *int `json:"since"`
} }
// GameType is the type of "game" (see GameType* consts) in the Game struct
type GameType int
// Valid GameType values
const (
GameTypeGame GameType = iota
GameTypeStreaming
)
// A Game struct holds the name of the "playing .." game for a user // A Game struct holds the name of the "playing .." game for a user
type Game struct { type Game struct {
Name string `json:"name"` Name string `json:"name"`
Type int `json:"type"` Type GameType `json:"type"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Details string `json:"details,omitempty"`
State string `json:"state,omitempty"`
TimeStamps TimeStamps `json:"timestamps,omitempty"`
Assets Assets `json:"assets,omitempty"`
ApplicationID string `json:"application_id,omitempty"`
Instance int8 `json:"instance,omitempty"`
// TODO: Party and Secrets (unknown structure)
} }
// UnmarshalJSON unmarshals json to Game struct // A TimeStamps struct contains start and end times used in the rich presence "playing .." Game
func (g *Game) UnmarshalJSON(bytes []byte) error { type TimeStamps struct {
temp := &struct { EndTimestamp int64 `json:"end,omitempty"`
Name json.Number `json:"name"` StartTimestamp int64 `json:"start,omitempty"`
Type json.RawMessage `json:"type"` }
URL string `json:"url"`
// UnmarshalJSON unmarshals JSON into TimeStamps struct
func (t *TimeStamps) UnmarshalJSON(b []byte) error {
temp := struct {
End float64 `json:"end,omitempty"`
Start float64 `json:"start,omitempty"`
}{} }{}
err := json.Unmarshal(bytes, temp) err := json.Unmarshal(b, &temp)
if err != nil { if err != nil {
return err return err
} }
g.URL = temp.URL t.EndTimestamp = int64(temp.End)
g.Name = temp.Name.String() t.StartTimestamp = int64(temp.Start)
if temp.Type != nil {
err = json.Unmarshal(temp.Type, &g.Type)
if err == nil {
return nil
}
s := ""
err = json.Unmarshal(temp.Type, &s)
if err == nil {
g.Type, err = strconv.Atoi(s)
}
return err
}
return nil return nil
} }
// An Assets struct contains assets and labels used in the rich presence "playing .." Game
type Assets struct {
LargeImageID string `json:"large_image,omitempty"`
SmallImageID string `json:"small_image,omitempty"`
LargeText string `json:"large_text,omitempty"`
SmallText string `json:"small_text,omitempty"`
}
// A Member stores user information for Guild members. // A Member stores user information for Guild members.
type Member struct { type Member struct {
GuildID string `json:"guild_id"` GuildID string `json:"guild_id"`
@ -383,7 +409,7 @@ type Settings struct {
DeveloperMode bool `json:"developer_mode"` DeveloperMode bool `json:"developer_mode"`
} }
// Status type defination // Status type definition
type Status string type Status string
// Constants for Status with the different current available status // Constants for Status with the different current available status

View File

@ -29,7 +29,9 @@ func (u *User) Mention() string {
} }
// AvatarURL returns a URL to the user's avatar. // AvatarURL returns a URL to the user's avatar.
// size: The size of the user's avatar as a power of two // size: The size of the user's avatar as a power of two
// if size is an empty string, no size parameter will
// be added to the URL.
func (u *User) AvatarURL(size string) string { func (u *User) AvatarURL(size string) string {
var URL string var URL string
if strings.HasPrefix(u.Avatar, "a_") { if strings.HasPrefix(u.Avatar, "a_") {
@ -38,5 +40,8 @@ func (u *User) AvatarURL(size string) string {
URL = EndpointUserAvatar(u.ID, u.Avatar) URL = EndpointUserAvatar(u.ID, u.Avatar)
} }
return URL + "?size=" + size if size != "" {
return URL + "?size=" + size
}
return URL
} }

View File

@ -13,7 +13,6 @@ import (
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"net" "net"
"strings" "strings"
"sync" "sync"
@ -69,7 +68,7 @@ type VoiceConnection struct {
voiceSpeakingUpdateHandlers []VoiceSpeakingUpdateHandler voiceSpeakingUpdateHandlers []VoiceSpeakingUpdateHandler
} }
// VoiceSpeakingUpdateHandler type provides a function defination for the // VoiceSpeakingUpdateHandler type provides a function definition for the
// VoiceSpeakingUpdate event // VoiceSpeakingUpdate event
type VoiceSpeakingUpdateHandler func(vc *VoiceConnection, vs *VoiceSpeakingUpdate) type VoiceSpeakingUpdateHandler func(vc *VoiceConnection, vs *VoiceSpeakingUpdate)
@ -104,7 +103,7 @@ func (v *VoiceConnection) Speaking(b bool) (err error) {
defer v.Unlock() defer v.Unlock()
if err != nil { if err != nil {
v.speaking = false v.speaking = false
log.Println("Speaking() write json error:", err) v.log(LogError, "Speaking() write json error:", err)
return return
} }
@ -181,7 +180,7 @@ func (v *VoiceConnection) Close() {
v.log(LogInformational, "closing udp") v.log(LogInformational, "closing udp")
err := v.udpConn.Close() err := v.udpConn.Close()
if err != nil { if err != nil {
log.Println("error closing udp connection: ", err) v.log(LogError, "error closing udp connection: ", err)
} }
v.udpConn = nil v.udpConn = nil
} }
@ -247,7 +246,7 @@ type voiceOP2 struct {
} }
// WaitUntilConnected waits for the Voice Connection to // WaitUntilConnected waits for the Voice Connection to
// become ready, if it does not become ready it retuns an err // become ready, if it does not become ready it returns an err
func (v *VoiceConnection) waitUntilConnected() error { func (v *VoiceConnection) waitUntilConnected() error {
v.log(LogInformational, "called") v.log(LogInformational, "called")
@ -858,7 +857,7 @@ func (v *VoiceConnection) reconnect() {
} }
if v.session.DataReady == false || v.session.wsConn == nil { if v.session.DataReady == false || v.session.wsConn == nil {
v.log(LogInformational, "cannot reconenct to channel %s with unready session", v.ChannelID) v.log(LogInformational, "cannot reconnect to channel %s with unready session", v.ChannelID)
continue continue
} }

View File

@ -15,6 +15,7 @@ import (
"compress/zlib" "compress/zlib"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"runtime" "runtime"
@ -45,19 +46,114 @@ type resumePacket struct {
} `json:"d"` } `json:"d"`
} }
// Open opens a websocket connection to Discord. // Open creates a websocket connection to Discord.
func (s *Session) Open() (err error) { // See: https://discordapp.com/developers/docs/topics/gateway#connecting
func (s *Session) Open() error {
s.log(LogInformational, "called") s.log(LogInformational, "called")
var err error
// Prevent Open or other major Session functions from
// being called while Open is still running.
s.Lock() s.Lock()
defer func() { defer s.Unlock()
// If the websock is already open, bail out here.
if s.wsConn != nil {
return ErrWSAlreadyOpen
}
// Get the gateway to use for the Websocket connection
if s.gateway == "" {
s.gateway, err = s.Gateway()
if err != nil { if err != nil {
s.Unlock() return err
}
// Add the version and encoding to the URL
s.gateway = s.gateway + "?v=" + APIVersion + "&encoding=json"
}
// Connect to the Gateway
s.log(LogInformational, "connecting to gateway %s", s.gateway)
header := http.Header{}
header.Add("accept-encoding", "zlib")
s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header)
if err != nil {
s.log(LogWarning, "error connecting to gateway %s, %s", s.gateway, err)
s.gateway = "" // clear cached gateway
s.wsConn = nil // Just to be safe.
return err
}
defer func() {
// because of this, all code below must set err to the error
// when exiting with an error :) Maybe someone has a better
// way :)
if err != nil {
s.wsConn.Close()
s.wsConn = nil
} }
}() }()
// The first response from Discord should be an Op 10 (Hello) Packet.
// When processed by onEvent the heartbeat goroutine will be started.
mt, m, err := s.wsConn.ReadMessage()
if err != nil {
return err
}
e, err := s.onEvent(mt, m)
if err != nil {
return err
}
if e.Operation != 10 {
err = fmt.Errorf("expecting Op 10, got Op %d instead", e.Operation)
return err
}
s.log(LogInformational, "Op 10 Hello Packet received from Discord")
s.LastHeartbeatAck = time.Now().UTC()
var h helloOp
if err = json.Unmarshal(e.RawData, &h); err != nil {
err = fmt.Errorf("error unmarshalling helloOp, %s", err)
return err
}
// Now we send either an Op 2 Identity if this is a brand new
// connection or Op 6 Resume if we are resuming an existing connection.
sequence := atomic.LoadInt64(s.sequence)
if s.sessionID == "" && sequence == 0 {
// Send Op 2 Identity Packet
err = s.identify()
if err != nil {
err = fmt.Errorf("error sending identify packet to gateway, %s, %s", s.gateway, err)
return err
}
} else {
// Send Op 6 Resume Packet
p := resumePacket{}
p.Op = 6
p.Data.Token = s.Token
p.Data.SessionID = s.sessionID
p.Data.Sequence = sequence
s.log(LogInformational, "sending resume packet to gateway")
s.wsMutex.Lock()
err = s.wsConn.WriteJSON(p)
s.wsMutex.Unlock()
if err != nil {
err = fmt.Errorf("error sending gateway resume packet, %s, %s", s.gateway, err)
return err
}
}
// A basic state is a hard requirement for Voice. // A basic state is a hard requirement for Voice.
// We create it here so the below READY/RESUMED packet can populate
// the state :)
// XXX: Move to New() func?
if s.State == nil { if s.State == nil {
state := NewState() state := NewState()
state.TrackChannels = false state.TrackChannels = false
@ -68,77 +164,42 @@ func (s *Session) Open() (err error) {
s.State = state s.State = state
} }
if s.wsConn != nil { // Now Discord should send us a READY or RESUMED packet.
err = ErrWSAlreadyOpen mt, m, err = s.wsConn.ReadMessage()
return if err != nil {
return err
} }
e, err = s.onEvent(mt, m)
if err != nil {
return err
}
if e.Type != `READY` && e.Type != `RESUMED` {
// This is not fatal, but it does not follow their API documentation.
s.log(LogWarning, "Expected READY/RESUMED, instead got:\n%#v\n", e)
}
s.log(LogInformational, "First Packet:\n%#v\n", e)
s.log(LogInformational, "We are now connected to Discord, emitting connect event")
s.handleEvent(connectEventType, &Connect{})
// A VoiceConnections map is a hard requirement for Voice.
// XXX: can this be moved to when opening a voice connection?
if s.VoiceConnections == nil { if s.VoiceConnections == nil {
s.log(LogInformational, "creating new VoiceConnections map") s.log(LogInformational, "creating new VoiceConnections map")
s.VoiceConnections = make(map[string]*VoiceConnection) s.VoiceConnections = make(map[string]*VoiceConnection)
} }
// Get the gateway to use for the Websocket connection // Create listening chan outside of listen, as it needs to happen inside the
if s.gateway == "" { // mutex lock and needs to exist before calling heartbeat and listen
s.gateway, err = s.Gateway() // go rountines.
if err != nil {
return
}
// Add the version and encoding to the URL
s.gateway = s.gateway + "?v=" + APIVersion + "&encoding=json"
}
header := http.Header{}
header.Add("accept-encoding", "zlib")
s.log(LogInformational, "connecting to gateway %s", s.gateway)
s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header)
if err != nil {
s.log(LogWarning, "error connecting to gateway %s, %s", s.gateway, err)
s.gateway = "" // clear cached gateway
// TODO: should we add a retry block here?
return
}
sequence := atomic.LoadInt64(s.sequence)
if s.sessionID != "" && sequence > 0 {
p := resumePacket{}
p.Op = 6
p.Data.Token = s.Token
p.Data.SessionID = s.sessionID
p.Data.Sequence = sequence
s.log(LogInformational, "sending resume packet to gateway")
err = s.wsConn.WriteJSON(p)
if err != nil {
s.log(LogWarning, "error sending gateway resume packet, %s, %s", s.gateway, err)
return
}
} else {
err = s.identify()
if err != nil {
s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err)
return
}
}
// Create listening outside of listen, as it needs to happen inside the mutex
// lock.
s.listening = make(chan interface{}) s.listening = make(chan interface{})
// Start sending heartbeats and reading messages from Discord.
go s.heartbeat(s.wsConn, s.listening, h.HeartbeatInterval)
go s.listen(s.wsConn, s.listening) go s.listen(s.wsConn, s.listening)
s.LastHeartbeatAck = time.Now().UTC()
s.Unlock()
s.log(LogInformational, "emit connect event")
s.handleEvent(connectEventType, &Connect{})
s.log(LogInformational, "exiting") s.log(LogInformational, "exiting")
return return nil
} }
// listen polls the websocket connection for events, it will stop when the // listen polls the websocket connection for events, it will stop when the
@ -249,7 +310,8 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}
} }
} }
type updateStatusData struct { // UpdateStatusData ia provided to UpdateStatusComplex()
type UpdateStatusData struct {
IdleSince *int `json:"since"` IdleSince *int `json:"since"`
Game *Game `json:"game"` Game *Game `json:"game"`
AFK bool `json:"afk"` AFK bool `json:"afk"`
@ -258,7 +320,7 @@ type updateStatusData struct {
type updateStatusOp struct { type updateStatusOp struct {
Op int `json:"op"` Op int `json:"op"`
Data updateStatusData `json:"d"` Data UpdateStatusData `json:"d"`
} }
// UpdateStreamingStatus is used to update the user's streaming status. // UpdateStreamingStatus is used to update the user's streaming status.
@ -270,13 +332,7 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
s.log(LogInformational, "called") s.log(LogInformational, "called")
s.RLock() usd := UpdateStatusData{
defer s.RUnlock()
if s.wsConn == nil {
return ErrWSNotFound
}
usd := updateStatusData{
Status: "online", Status: "online",
} }
@ -285,9 +341,9 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
} }
if game != "" { if game != "" {
gameType := 0 gameType := GameTypeGame
if url != "" { if url != "" {
gameType = 1 gameType = GameTypeStreaming
} }
usd.Game = &Game{ usd.Game = &Game{
Name: game, Name: game,
@ -296,6 +352,18 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
} }
} }
return s.UpdateStatusComplex(usd)
}
// UpdateStatusComplex allows for sending the raw status update data untouched by discordgo.
func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) {
s.RLock()
defer s.RUnlock()
if s.wsConn == nil {
return ErrWSNotFound
}
s.wsMutex.Lock() s.wsMutex.Lock()
err = s.wsConn.WriteJSON(updateStatusOp{3, usd}) err = s.wsConn.WriteJSON(updateStatusOp{3, usd})
s.wsMutex.Unlock() s.wsMutex.Unlock()
@ -357,9 +425,7 @@ func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err err
// //
// If you use the AddHandler() function to register a handler for the // If you use the AddHandler() function to register a handler for the
// "OnEvent" event then all events will be passed to that handler. // "OnEvent" event then all events will be passed to that handler.
// func (s *Session) onEvent(messageType int, message []byte) (*Event, error) {
// TODO: You may also register a custom event handler entirely using...
func (s *Session) onEvent(messageType int, message []byte) {
var err error var err error
var reader io.Reader var reader io.Reader
@ -371,7 +437,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
z, err2 := zlib.NewReader(reader) z, err2 := zlib.NewReader(reader)
if err2 != nil { if err2 != nil {
s.log(LogError, "error uncompressing websocket message, %s", err) s.log(LogError, "error uncompressing websocket message, %s", err)
return return nil, err2
} }
defer func() { defer func() {
@ -389,7 +455,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
decoder := json.NewDecoder(reader) decoder := json.NewDecoder(reader)
if err = decoder.Decode(&e); err != nil { if err = decoder.Decode(&e); err != nil {
s.log(LogError, "error decoding websocket message, %s", err) s.log(LogError, "error decoding websocket message, %s", err)
return return e, err
} }
s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s\n\n", e.Operation, e.Sequence, e.Type, string(e.RawData)) s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s\n\n", e.Operation, e.Sequence, e.Type, string(e.RawData))
@ -403,10 +469,10 @@ func (s *Session) onEvent(messageType int, message []byte) {
s.wsMutex.Unlock() s.wsMutex.Unlock()
if err != nil { if err != nil {
s.log(LogError, "error sending heartbeat in response to Op1") s.log(LogError, "error sending heartbeat in response to Op1")
return return e, err
} }
return return e, nil
} }
// Reconnect // Reconnect
@ -415,7 +481,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
s.log(LogInformational, "Closing and reconnecting in response to Op7") s.log(LogInformational, "Closing and reconnecting in response to Op7")
s.Close() s.Close()
s.reconnect() s.reconnect()
return return e, nil
} }
// Invalid Session // Invalid Session
@ -427,20 +493,15 @@ func (s *Session) onEvent(messageType int, message []byte) {
err = s.identify() err = s.identify()
if err != nil { if err != nil {
s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err) s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err)
return return e, err
} }
return return e, nil
} }
if e.Operation == 10 { if e.Operation == 10 {
var h helloOp // Op10 is handled by Open()
if err = json.Unmarshal(e.RawData, &h); err != nil { return e, nil
s.log(LogError, "error unmarshalling helloOp, %s", err)
} else {
go s.heartbeat(s.wsConn, s.listening, h.HeartbeatInterval)
}
return
} }
if e.Operation == 11 { if e.Operation == 11 {
@ -448,7 +509,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
s.LastHeartbeatAck = time.Now().UTC() s.LastHeartbeatAck = time.Now().UTC()
s.Unlock() s.Unlock()
s.log(LogInformational, "got heartbeat ACK") s.log(LogInformational, "got heartbeat ACK")
return return e, nil
} }
// Do not try to Dispatch a non-Dispatch Message // Do not try to Dispatch a non-Dispatch Message
@ -456,7 +517,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
// But we probably should be doing something with them. // But we probably should be doing something with them.
// TEMP // TEMP
s.log(LogWarning, "unknown Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message)) s.log(LogWarning, "unknown Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message))
return return e, nil
} }
// Store the message sequence // Store the message sequence
@ -485,6 +546,8 @@ func (s *Session) onEvent(messageType int, message []byte) {
// For legacy reasons, we send the raw event also, this could be useful for handling unknown events. // For legacy reasons, we send the raw event also, this could be useful for handling unknown events.
s.handleEvent(eventEventType, e) s.handleEvent(eventEventType, e)
return e, nil
} }
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
@ -610,7 +673,7 @@ func (s *Session) onVoiceServerUpdate(st *VoiceServerUpdate) {
voice.GuildID = st.GuildID voice.GuildID = st.GuildID
voice.Unlock() voice.Unlock()
// Open a conenction to the voice server // Open a connection to the voice server
err := voice.open() err := voice.open()
if err != nil { if err != nil {
s.log(LogError, "onVoiceServerUpdate voice.open, %s", err) s.log(LogError, "onVoiceServerUpdate voice.open, %s", err)

77
vendor/github.com/gorilla/websocket/proxy.go generated vendored Normal file
View File

@ -0,0 +1,77 @@
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bufio"
"encoding/base64"
"errors"
"net"
"net/http"
"net/url"
"strings"
)
type netDialerFunc func(netowrk, addr string) (net.Conn, error)
func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) {
return fn(network, addr)
}
func init() {
proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) {
return &httpProxyDialer{proxyURL: proxyURL, fowardDial: forwardDialer.Dial}, nil
})
}
type httpProxyDialer struct {
proxyURL *url.URL
fowardDial func(network, addr string) (net.Conn, error)
}
func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) {
hostPort, _ := hostPortNoPort(hpd.proxyURL)
conn, err := hpd.fowardDial(network, hostPort)
if err != nil {
return nil, err
}
connectHeader := make(http.Header)
if user := hpd.proxyURL.User; user != nil {
proxyUser := user.Username()
if proxyPassword, passwordSet := user.Password(); passwordSet {
credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword))
connectHeader.Set("Proxy-Authorization", "Basic "+credential)
}
}
connectReq := &http.Request{
Method: "CONNECT",
URL: &url.URL{Opaque: addr},
Host: addr,
Header: connectHeader,
}
if err := connectReq.Write(conn); err != nil {
conn.Close()
return nil, err
}
// Read response. It's OK to use and discard buffered reader here becaue
// the remote server does not speak until spoken to.
br := bufio.NewReader(conn)
resp, err := http.ReadResponse(br, connectReq)
if err != nil {
conn.Close()
return nil, err
}
if resp.StatusCode != 200 {
conn.Close()
f := strings.SplitN(resp.Status, " ", 2)
return nil, errors.New(f[1])
}
return conn, nil
}

473
vendor/github.com/gorilla/websocket/x_net_proxy.go generated vendored Normal file
View File

@ -0,0 +1,473 @@
// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.
//go:generate bundle -o x_net_proxy.go golang.org/x/net/proxy
// Package proxy provides support for a variety of protocols to proxy network
// data.
//
package websocket
import (
"errors"
"io"
"net"
"net/url"
"os"
"strconv"
"strings"
"sync"
)
type proxy_direct struct{}
// Direct is a direct proxy: one that makes network connections directly.
var proxy_Direct = proxy_direct{}
func (proxy_direct) Dial(network, addr string) (net.Conn, error) {
return net.Dial(network, addr)
}
// A PerHost directs connections to a default Dialer unless the host name
// requested matches one of a number of exceptions.
type proxy_PerHost struct {
def, bypass proxy_Dialer
bypassNetworks []*net.IPNet
bypassIPs []net.IP
bypassZones []string
bypassHosts []string
}
// NewPerHost returns a PerHost Dialer that directs connections to either
// defaultDialer or bypass, depending on whether the connection matches one of
// the configured rules.
func proxy_NewPerHost(defaultDialer, bypass proxy_Dialer) *proxy_PerHost {
return &proxy_PerHost{
def: defaultDialer,
bypass: bypass,
}
}
// Dial connects to the address addr on the given network through either
// defaultDialer or bypass.
func (p *proxy_PerHost) Dial(network, addr string) (c net.Conn, err error) {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
return p.dialerForRequest(host).Dial(network, addr)
}
func (p *proxy_PerHost) dialerForRequest(host string) proxy_Dialer {
if ip := net.ParseIP(host); ip != nil {
for _, net := range p.bypassNetworks {
if net.Contains(ip) {
return p.bypass
}
}
for _, bypassIP := range p.bypassIPs {
if bypassIP.Equal(ip) {
return p.bypass
}
}
return p.def
}
for _, zone := range p.bypassZones {
if strings.HasSuffix(host, zone) {
return p.bypass
}
if host == zone[1:] {
// For a zone ".example.com", we match "example.com"
// too.
return p.bypass
}
}
for _, bypassHost := range p.bypassHosts {
if bypassHost == host {
return p.bypass
}
}
return p.def
}
// AddFromString parses a string that contains comma-separated values
// specifying hosts that should use the bypass proxy. Each value is either an
// IP address, a CIDR range, a zone (*.example.com) or a host name
// (localhost). A best effort is made to parse the string and errors are
// ignored.
func (p *proxy_PerHost) AddFromString(s string) {
hosts := strings.Split(s, ",")
for _, host := range hosts {
host = strings.TrimSpace(host)
if len(host) == 0 {
continue
}
if strings.Contains(host, "/") {
// We assume that it's a CIDR address like 127.0.0.0/8
if _, net, err := net.ParseCIDR(host); err == nil {
p.AddNetwork(net)
}
continue
}
if ip := net.ParseIP(host); ip != nil {
p.AddIP(ip)
continue
}
if strings.HasPrefix(host, "*.") {
p.AddZone(host[1:])
continue
}
p.AddHost(host)
}
}
// AddIP specifies an IP address that will use the bypass proxy. Note that
// this will only take effect if a literal IP address is dialed. A connection
// to a named host will never match an IP.
func (p *proxy_PerHost) AddIP(ip net.IP) {
p.bypassIPs = append(p.bypassIPs, ip)
}
// AddNetwork specifies an IP range that will use the bypass proxy. Note that
// this will only take effect if a literal IP address is dialed. A connection
// to a named host will never match.
func (p *proxy_PerHost) AddNetwork(net *net.IPNet) {
p.bypassNetworks = append(p.bypassNetworks, net)
}
// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of
// "example.com" matches "example.com" and all of its subdomains.
func (p *proxy_PerHost) AddZone(zone string) {
if strings.HasSuffix(zone, ".") {
zone = zone[:len(zone)-1]
}
if !strings.HasPrefix(zone, ".") {
zone = "." + zone
}
p.bypassZones = append(p.bypassZones, zone)
}
// AddHost specifies a host name that will use the bypass proxy.
func (p *proxy_PerHost) AddHost(host string) {
if strings.HasSuffix(host, ".") {
host = host[:len(host)-1]
}
p.bypassHosts = append(p.bypassHosts, host)
}
// A Dialer is a means to establish a connection.
type proxy_Dialer interface {
// Dial connects to the given address via the proxy.
Dial(network, addr string) (c net.Conn, err error)
}
// Auth contains authentication parameters that specific Dialers may require.
type proxy_Auth struct {
User, Password string
}
// FromEnvironment returns the dialer specified by the proxy related variables in
// the environment.
func proxy_FromEnvironment() proxy_Dialer {
allProxy := proxy_allProxyEnv.Get()
if len(allProxy) == 0 {
return proxy_Direct
}
proxyURL, err := url.Parse(allProxy)
if err != nil {
return proxy_Direct
}
proxy, err := proxy_FromURL(proxyURL, proxy_Direct)
if err != nil {
return proxy_Direct
}
noProxy := proxy_noProxyEnv.Get()
if len(noProxy) == 0 {
return proxy
}
perHost := proxy_NewPerHost(proxy, proxy_Direct)
perHost.AddFromString(noProxy)
return perHost
}
// proxySchemes is a map from URL schemes to a function that creates a Dialer
// from a URL with such a scheme.
var proxy_proxySchemes map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error)
// RegisterDialerType takes a URL scheme and a function to generate Dialers from
// a URL with that scheme and a forwarding Dialer. Registered schemes are used
// by FromURL.
func proxy_RegisterDialerType(scheme string, f func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) {
if proxy_proxySchemes == nil {
proxy_proxySchemes = make(map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error))
}
proxy_proxySchemes[scheme] = f
}
// FromURL returns a Dialer given a URL specification and an underlying
// Dialer for it to make network requests.
func proxy_FromURL(u *url.URL, forward proxy_Dialer) (proxy_Dialer, error) {
var auth *proxy_Auth
if u.User != nil {
auth = new(proxy_Auth)
auth.User = u.User.Username()
if p, ok := u.User.Password(); ok {
auth.Password = p
}
}
switch u.Scheme {
case "socks5":
return proxy_SOCKS5("tcp", u.Host, auth, forward)
}
// If the scheme doesn't match any of the built-in schemes, see if it
// was registered by another package.
if proxy_proxySchemes != nil {
if f, ok := proxy_proxySchemes[u.Scheme]; ok {
return f(u, forward)
}
}
return nil, errors.New("proxy: unknown scheme: " + u.Scheme)
}
var (
proxy_allProxyEnv = &proxy_envOnce{
names: []string{"ALL_PROXY", "all_proxy"},
}
proxy_noProxyEnv = &proxy_envOnce{
names: []string{"NO_PROXY", "no_proxy"},
}
)
// envOnce looks up an environment variable (optionally by multiple
// names) once. It mitigates expensive lookups on some platforms
// (e.g. Windows).
// (Borrowed from net/http/transport.go)
type proxy_envOnce struct {
names []string
once sync.Once
val string
}
func (e *proxy_envOnce) Get() string {
e.once.Do(e.init)
return e.val
}
func (e *proxy_envOnce) init() {
for _, n := range e.names {
e.val = os.Getenv(n)
if e.val != "" {
return
}
}
}
// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address
// with an optional username and password. See RFC 1928 and RFC 1929.
func proxy_SOCKS5(network, addr string, auth *proxy_Auth, forward proxy_Dialer) (proxy_Dialer, error) {
s := &proxy_socks5{
network: network,
addr: addr,
forward: forward,
}
if auth != nil {
s.user = auth.User
s.password = auth.Password
}
return s, nil
}
type proxy_socks5 struct {
user, password string
network, addr string
forward proxy_Dialer
}
const proxy_socks5Version = 5
const (
proxy_socks5AuthNone = 0
proxy_socks5AuthPassword = 2
)
const proxy_socks5Connect = 1
const (
proxy_socks5IP4 = 1
proxy_socks5Domain = 3
proxy_socks5IP6 = 4
)
var proxy_socks5Errors = []string{
"",
"general failure",
"connection forbidden",
"network unreachable",
"host unreachable",
"connection refused",
"TTL expired",
"command not supported",
"address type not supported",
}
// Dial connects to the address addr on the given network via the SOCKS5 proxy.
func (s *proxy_socks5) Dial(network, addr string) (net.Conn, error) {
switch network {
case "tcp", "tcp6", "tcp4":
default:
return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network)
}
conn, err := s.forward.Dial(s.network, s.addr)
if err != nil {
return nil, err
}
if err := s.connect(conn, addr); err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
// connect takes an existing connection to a socks5 proxy server,
// and commands the server to extend that connection to target,
// which must be a canonical address with a host and port.
func (s *proxy_socks5) connect(conn net.Conn, target string) error {
host, portStr, err := net.SplitHostPort(target)
if err != nil {
return err
}
port, err := strconv.Atoi(portStr)
if err != nil {
return errors.New("proxy: failed to parse port number: " + portStr)
}
if port < 1 || port > 0xffff {
return errors.New("proxy: port number out of range: " + portStr)
}
// the size here is just an estimate
buf := make([]byte, 0, 6+len(host))
buf = append(buf, proxy_socks5Version)
if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 {
buf = append(buf, 2 /* num auth methods */, proxy_socks5AuthNone, proxy_socks5AuthPassword)
} else {
buf = append(buf, 1 /* num auth methods */, proxy_socks5AuthNone)
}
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if buf[0] != 5 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0])))
}
if buf[1] == 0xff {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication")
}
// See RFC 1929
if buf[1] == proxy_socks5AuthPassword {
buf = buf[:0]
buf = append(buf, 1 /* password protocol version */)
buf = append(buf, uint8(len(s.user)))
buf = append(buf, s.user...)
buf = append(buf, uint8(len(s.password)))
buf = append(buf, s.password...)
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if buf[1] != 0 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password")
}
}
buf = buf[:0]
buf = append(buf, proxy_socks5Version, proxy_socks5Connect, 0 /* reserved */)
if ip := net.ParseIP(host); ip != nil {
if ip4 := ip.To4(); ip4 != nil {
buf = append(buf, proxy_socks5IP4)
ip = ip4
} else {
buf = append(buf, proxy_socks5IP6)
}
buf = append(buf, ip...)
} else {
if len(host) > 255 {
return errors.New("proxy: destination host name too long: " + host)
}
buf = append(buf, proxy_socks5Domain)
buf = append(buf, byte(len(host)))
buf = append(buf, host...)
}
buf = append(buf, byte(port>>8), byte(port))
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
failure := "unknown error"
if int(buf[1]) < len(proxy_socks5Errors) {
failure = proxy_socks5Errors[buf[1]]
}
if len(failure) > 0 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure)
}
bytesToDiscard := 0
switch buf[3] {
case proxy_socks5IP4:
bytesToDiscard = net.IPv4len
case proxy_socks5IP6:
bytesToDiscard = net.IPv6len
case proxy_socks5Domain:
_, err := io.ReadFull(conn, buf[:1])
if err != nil {
return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
bytesToDiscard = int(buf[0])
default:
return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr)
}
if cap(buf) < bytesToDiscard {
buf = make([]byte, bytesToDiscard)
} else {
buf = buf[:bytesToDiscard]
}
if _, err := io.ReadFull(conn, buf); err != nil {
return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
// Also need to discard the port number
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
return nil
}

View File

@ -274,13 +274,6 @@ func (c *context) Param(name string) string {
if n == name { if n == name {
return c.pvalues[i] return c.pvalues[i]
} }
// Param name with aliases
for _, p := range strings.Split(n, ",") {
if p == name {
return c.pvalues[i]
}
}
} }
} }
return "" return ""
@ -494,14 +487,9 @@ func (c *context) Stream(code int, contentType string, r io.Reader) (err error)
} }
func (c *context) File(file string) (err error) { func (c *context) File(file string) (err error) {
file, err = url.QueryUnescape(file) // Issue #839
if err != nil {
return
}
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
return ErrNotFound return NotFoundHandler(c)
} }
defer f.Close() defer f.Close()
@ -510,7 +498,7 @@ func (c *context) File(file string) (err error) {
file = filepath.Join(file, indexPage) file = filepath.Join(file, indexPage)
f, err = os.Open(file) f, err = os.Open(file)
if err != nil { if err != nil {
return ErrNotFound return NotFoundHandler(c)
} }
defer f.Close() defer f.Close()
if fi, err = f.Stat(); err != nil { if fi, err = f.Stat(); err != nil {
@ -530,7 +518,7 @@ func (c *context) Inline(file, name string) (err error) {
} }
func (c *context) contentDisposition(file, name, dispositionType string) (err error) { func (c *context) contentDisposition(file, name, dispositionType string) (err error) {
c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%s", dispositionType, name)) c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%q", dispositionType, name))
c.File(file) c.File(file)
return return
} }

View File

@ -72,29 +72,31 @@ type (
TLSServer *http.Server TLSServer *http.Server
Listener net.Listener Listener net.Listener
TLSListener net.Listener TLSListener net.Listener
AutoTLSManager autocert.Manager
DisableHTTP2 bool DisableHTTP2 bool
Debug bool Debug bool
HideBanner bool HideBanner bool
HidePort bool
HTTPErrorHandler HTTPErrorHandler HTTPErrorHandler HTTPErrorHandler
Binder Binder Binder Binder
Validator Validator Validator Validator
Renderer Renderer Renderer Renderer
AutoTLSManager autocert.Manager
// Mutex sync.RWMutex // Mutex sync.RWMutex
Logger Logger Logger Logger
} }
// Route contains a handler and information for matching against requests. // Route contains a handler and information for matching against requests.
Route struct { Route struct {
Method string `json:"method"` Method string `json:"method"`
Path string `json:"path"` Path string `json:"path"`
Handler string `json:"handler"` Name string `json:"name"`
} }
// HTTPError represents an error that occurred while handling a request. // HTTPError represents an error that occurred while handling a request.
HTTPError struct { HTTPError struct {
Code int Code int
Message interface{} Message interface{}
Inner error // Stores the error returned by an external dependency
} }
// MiddlewareFunc defines a function to process middleware. // MiddlewareFunc defines a function to process middleware.
@ -121,7 +123,7 @@ type (
// i is the interface for Echo and Group. // i is the interface for Echo and Group.
i interface { i interface {
GET(string, HandlerFunc, ...MiddlewareFunc) GET(string, HandlerFunc, ...MiddlewareFunc) *Route
} }
) )
@ -212,7 +214,7 @@ const (
) )
const ( const (
version = "3.1.0" version = "3.2.6"
website = "https://echo.labstack.com" website = "https://echo.labstack.com"
// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo // http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
banner = ` banner = `
@ -282,7 +284,7 @@ func New() (e *Echo) {
e.TLSServer.Handler = e e.TLSServer.Handler = e
e.HTTPErrorHandler = e.DefaultHTTPErrorHandler e.HTTPErrorHandler = e.DefaultHTTPErrorHandler
e.Binder = &DefaultBinder{} e.Binder = &DefaultBinder{}
e.Logger.SetLevel(log.OFF) e.Logger.SetLevel(log.ERROR)
e.stdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0) e.stdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0)
e.pool.New = func() interface{} { e.pool.New = func() interface{} {
return e.NewContext(nil, nil) return e.NewContext(nil, nil)
@ -319,6 +321,9 @@ func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
if he, ok := err.(*HTTPError); ok { if he, ok := err.(*HTTPError); ok {
code = he.Code code = he.Code
msg = he.Message msg = he.Message
if he.Inner != nil {
msg = fmt.Sprintf("%v, %v", err, he.Inner)
}
} else if e.Debug { } else if e.Debug {
msg = err.Error() msg = err.Error()
} else { } else {
@ -328,19 +333,19 @@ func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
msg = Map{"message": msg} msg = Map{"message": msg}
} }
e.Logger.Error(err)
// Send response
if !c.Response().Committed { if !c.Response().Committed {
if c.Request().Method == HEAD { // Issue #608 if c.Request().Method == HEAD { // Issue #608
if err := c.NoContent(code); err != nil { err = c.NoContent(code)
goto ERROR
}
} else { } else {
if err := c.JSON(code, msg); err != nil { err = c.JSON(code, msg)
goto ERROR }
} if err != nil {
e.Logger.Error(err)
} }
} }
ERROR:
e.Logger.Error(err)
} }
// Pre adds middleware to the chain which is run before router. // Pre adds middleware to the chain which is run before router.
@ -355,104 +360,114 @@ func (e *Echo) Use(middleware ...MiddlewareFunc) {
// CONNECT registers a new CONNECT route for a path with matching handler in the // CONNECT registers a new CONNECT route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(CONNECT, path, h, m...) return e.Add(CONNECT, path, h, m...)
} }
// DELETE registers a new DELETE route for a path with matching handler in the router // DELETE registers a new DELETE route for a path with matching handler in the router
// with optional route-level middleware. // with optional route-level middleware.
func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(DELETE, path, h, m...) return e.Add(DELETE, path, h, m...)
} }
// GET registers a new GET route for a path with matching handler in the router // GET registers a new GET route for a path with matching handler in the router
// with optional route-level middleware. // with optional route-level middleware.
func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(GET, path, h, m...) return e.Add(GET, path, h, m...)
} }
// HEAD registers a new HEAD route for a path with matching handler in the // HEAD registers a new HEAD route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(HEAD, path, h, m...) return e.Add(HEAD, path, h, m...)
} }
// OPTIONS registers a new OPTIONS route for a path with matching handler in the // OPTIONS registers a new OPTIONS route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(OPTIONS, path, h, m...) return e.Add(OPTIONS, path, h, m...)
} }
// PATCH registers a new PATCH route for a path with matching handler in the // PATCH registers a new PATCH route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(PATCH, path, h, m...) return e.Add(PATCH, path, h, m...)
} }
// POST registers a new POST route for a path with matching handler in the // POST registers a new POST route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(POST, path, h, m...) return e.Add(POST, path, h, m...)
} }
// PUT registers a new PUT route for a path with matching handler in the // PUT registers a new PUT route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(PUT, path, h, m...) return e.Add(PUT, path, h, m...)
} }
// TRACE registers a new TRACE route for a path with matching handler in the // TRACE registers a new TRACE route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(TRACE, path, h, m...) return e.Add(TRACE, path, h, m...)
} }
// Any registers a new route for all HTTP methods and path with matching handler // Any registers a new route for all HTTP methods and path with matching handler
// in the router with optional route-level middleware. // in the router with optional route-level middleware.
func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) { func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
for _, m := range methods { routes := make([]*Route, len(methods))
e.add(m, path, handler, middleware...) for i, m := range methods {
routes[i] = e.Add(m, path, handler, middleware...)
} }
return routes
} }
// Match registers a new route for multiple HTTP methods and path with matching // Match registers a new route for multiple HTTP methods and path with matching
// handler in the router with optional route-level middleware. // handler in the router with optional route-level middleware.
func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
for _, m := range methods { routes := make([]*Route, len(methods))
e.add(m, path, handler, middleware...) for i, m := range methods {
routes[i] = e.Add(m, path, handler, middleware...)
} }
return routes
} }
// Static registers a new route with path prefix to serve static files from the // Static registers a new route with path prefix to serve static files from the
// provided root directory. // provided root directory.
func (e *Echo) Static(prefix, root string) { func (e *Echo) Static(prefix, root string) *Route {
if root == "" { if root == "" {
root = "." // For security we want to restrict to CWD. root = "." // For security we want to restrict to CWD.
} }
static(e, prefix, root) return static(e, prefix, root)
} }
func static(i i, prefix, root string) { func static(i i, prefix, root string) *Route {
h := func(c Context) error { h := func(c Context) error {
name := filepath.Join(root, path.Clean("/"+c.Param("*"))) // "/"+ for security p, err := PathUnescape(c.Param("*"))
if err != nil {
return err
}
name := filepath.Join(root, path.Clean("/"+p)) // "/"+ for security
return c.File(name) return c.File(name)
} }
i.GET(prefix, h) i.GET(prefix, h)
if prefix == "/" { if prefix == "/" {
i.GET(prefix+"*", h) return i.GET(prefix+"*", h)
} else {
i.GET(prefix+"/*", h)
} }
return i.GET(prefix+"/*", h)
} }
// File registers a new route with path to serve a static file. // File registers a new route with path to serve a static file.
func (e *Echo) File(path, file string) { func (e *Echo) File(path, file string) *Route {
e.GET(path, func(c Context) error { return e.GET(path, func(c Context) error {
return c.File(file) return c.File(file)
}) })
} }
func (e *Echo) add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { // Add registers a new route for an HTTP method and path with matching handler
// in the router with optional route-level middleware.
func (e *Echo) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
name := handlerName(handler) name := handlerName(handler)
e.router.Add(method, path, func(c Context) error { e.router.Add(method, path, func(c Context) error {
h := handler h := handler
@ -463,11 +478,12 @@ func (e *Echo) add(method, path string, handler HandlerFunc, middleware ...Middl
return h(c) return h(c)
}) })
r := &Route{ r := &Route{
Method: method, Method: method,
Path: path, Path: path,
Handler: name, Name: name,
} }
e.router.routes[method+path] = r e.router.routes[method+path] = r
return r
} }
// Group creates a new router group with prefix and optional group-level middleware. // Group creates a new router group with prefix and optional group-level middleware.
@ -479,12 +495,22 @@ func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) {
// URI generates a URI from handler. // URI generates a URI from handler.
func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string { func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string {
name := handlerName(handler)
return e.Reverse(name, params...)
}
// URL is an alias for `URI` function.
func (e *Echo) URL(h HandlerFunc, params ...interface{}) string {
return e.URI(h, params...)
}
// Reverse generates an URL from route name and provided parameters.
func (e *Echo) Reverse(name string, params ...interface{}) string {
uri := new(bytes.Buffer) uri := new(bytes.Buffer)
ln := len(params) ln := len(params)
n := 0 n := 0
name := handlerName(handler)
for _, r := range e.router.routes { for _, r := range e.router.routes {
if r.Handler == name { if r.Name == name {
for i, l := 0, len(r.Path); i < l; i++ { for i, l := 0, len(r.Path); i < l; i++ {
if r.Path[i] == ':' && n < ln { if r.Path[i] == ':' && n < ln {
for ; i < l && r.Path[i] != '/'; i++ { for ; i < l && r.Path[i] != '/'; i++ {
@ -502,11 +528,6 @@ func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string {
return uri.String() return uri.String()
} }
// URL is an alias for `URI` function.
func (e *Echo) URL(h HandlerFunc, params ...interface{}) string {
return e.URI(h, params...)
}
// Routes returns the registered routes. // Routes returns the registered routes.
func (e *Echo) Routes() []*Route { func (e *Echo) Routes() []*Route {
routes := []*Route{} routes := []*Route{}
@ -624,7 +645,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
return err return err
} }
} }
if !e.HideBanner { if !e.HidePort {
e.colorer.Printf("⇨ http server started on %s\n", e.colorer.Green(e.Listener.Addr())) e.colorer.Printf("⇨ http server started on %s\n", e.colorer.Green(e.Listener.Addr()))
} }
return s.Serve(e.Listener) return s.Serve(e.Listener)
@ -636,7 +657,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
} }
e.TLSListener = tls.NewListener(l, s.TLSConfig) e.TLSListener = tls.NewListener(l, s.TLSConfig)
} }
if !e.HideBanner { if !e.HidePort {
e.colorer.Printf("⇨ https server started on %s\n", e.colorer.Green(e.TLSListener.Addr())) e.colorer.Printf("⇨ https server started on %s\n", e.colorer.Green(e.TLSListener.Addr()))
} }
return s.Serve(e.TLSListener) return s.Serve(e.TLSListener)
@ -653,7 +674,7 @@ func NewHTTPError(code int, message ...interface{}) *HTTPError {
// Error makes it compatible with `error` interface. // Error makes it compatible with `error` interface.
func (he *HTTPError) Error() string { func (he *HTTPError) Error() string {
return fmt.Sprintf("code=%d, message=%s", he.Code, he.Message) return fmt.Sprintf("code=%d, message=%v", he.Code, he.Message)
} }
// WrapHandler wraps `http.Handler` into `echo.HandlerFunc`. // WrapHandler wraps `http.Handler` into `echo.HandlerFunc`.

View File

@ -20,68 +20,74 @@ func (g *Group) Use(middleware ...MiddlewareFunc) {
g.middleware = append(g.middleware, middleware...) g.middleware = append(g.middleware, middleware...)
// Allow all requests to reach the group as they might get dropped if router // Allow all requests to reach the group as they might get dropped if router
// doesn't find a match, making none of the group middleware process. // doesn't find a match, making none of the group middleware process.
g.echo.Any(path.Clean(g.prefix+"/*"), func(c Context) error { for _, p := range []string{"", "/*"} {
return ErrNotFound g.echo.Any(path.Clean(g.prefix+p), func(c Context) error {
}, g.middleware...) return NotFoundHandler(c)
}, g.middleware...)
}
} }
// CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. // CONNECT implements `Echo#CONNECT()` for sub-routes within the Group.
func (g *Group) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(CONNECT, path, h, m...) return g.Add(CONNECT, path, h, m...)
} }
// DELETE implements `Echo#DELETE()` for sub-routes within the Group. // DELETE implements `Echo#DELETE()` for sub-routes within the Group.
func (g *Group) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(DELETE, path, h, m...) return g.Add(DELETE, path, h, m...)
} }
// GET implements `Echo#GET()` for sub-routes within the Group. // GET implements `Echo#GET()` for sub-routes within the Group.
func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(GET, path, h, m...) return g.Add(GET, path, h, m...)
} }
// HEAD implements `Echo#HEAD()` for sub-routes within the Group. // HEAD implements `Echo#HEAD()` for sub-routes within the Group.
func (g *Group) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(HEAD, path, h, m...) return g.Add(HEAD, path, h, m...)
} }
// OPTIONS implements `Echo#OPTIONS()` for sub-routes within the Group. // OPTIONS implements `Echo#OPTIONS()` for sub-routes within the Group.
func (g *Group) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(OPTIONS, path, h, m...) return g.Add(OPTIONS, path, h, m...)
} }
// PATCH implements `Echo#PATCH()` for sub-routes within the Group. // PATCH implements `Echo#PATCH()` for sub-routes within the Group.
func (g *Group) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(PATCH, path, h, m...) return g.Add(PATCH, path, h, m...)
} }
// POST implements `Echo#POST()` for sub-routes within the Group. // POST implements `Echo#POST()` for sub-routes within the Group.
func (g *Group) POST(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) POST(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(POST, path, h, m...) return g.Add(POST, path, h, m...)
} }
// PUT implements `Echo#PUT()` for sub-routes within the Group. // PUT implements `Echo#PUT()` for sub-routes within the Group.
func (g *Group) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(PUT, path, h, m...) return g.Add(PUT, path, h, m...)
} }
// TRACE implements `Echo#TRACE()` for sub-routes within the Group. // TRACE implements `Echo#TRACE()` for sub-routes within the Group.
func (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(TRACE, path, h, m...) return g.Add(TRACE, path, h, m...)
} }
// Any implements `Echo#Any()` for sub-routes within the Group. // Any implements `Echo#Any()` for sub-routes within the Group.
func (g *Group) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) { func (g *Group) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
for _, m := range methods { routes := make([]*Route, len(methods))
g.add(m, path, handler, middleware...) for i, m := range methods {
routes[i] = g.Add(m, path, handler, middleware...)
} }
return routes
} }
// Match implements `Echo#Match()` for sub-routes within the Group. // Match implements `Echo#Match()` for sub-routes within the Group.
func (g *Group) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { func (g *Group) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
for _, m := range methods { routes := make([]*Route, len(methods))
g.add(m, path, handler, middleware...) for i, m := range methods {
routes[i] = g.Add(m, path, handler, middleware...)
} }
return routes
} }
// Group creates a new sub-group with prefix and optional sub-group-level middleware. // Group creates a new sub-group with prefix and optional sub-group-level middleware.
@ -102,12 +108,13 @@ func (g *Group) File(path, file string) {
g.echo.File(g.prefix+path, file) g.echo.File(g.prefix+path, file)
} }
func (g *Group) add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { // Add implements `Echo#Add()` for sub-routes within the Group.
func (g *Group) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
// Combine into a new slice to avoid accidentally passing the same slice for // Combine into a new slice to avoid accidentally passing the same slice for
// multiple routes, which would lead to later add() calls overwriting the // multiple routes, which would lead to later add() calls overwriting the
// middleware from earlier calls. // middleware from earlier calls.
m := []MiddlewareFunc{} m := []MiddlewareFunc{}
m = append(m, g.middleware...) m = append(m, g.middleware...)
m = append(m, middleware...) m = append(m, middleware...)
g.echo.add(method, g.prefix+path, handler, m...) return g.echo.Add(method, g.prefix+path, handler, m...)
} }

View File

@ -3,6 +3,7 @@ package middleware
import ( import (
"encoding/base64" "encoding/base64"
"strconv" "strconv"
"strings"
"github.com/labstack/echo" "github.com/labstack/echo"
) )
@ -27,7 +28,7 @@ type (
) )
const ( const (
basic = "Basic" basic = "basic"
defaultRealm = "Restricted" defaultRealm = "Restricted"
) )
@ -54,7 +55,7 @@ func BasicAuth(fn BasicAuthValidator) echo.MiddlewareFunc {
func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc { func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
// Defaults // Defaults
if config.Validator == nil { if config.Validator == nil {
panic("basic-auth middleware requires a validator function") panic("echo: basic-auth middleware requires a validator function")
} }
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultBasicAuthConfig.Skipper config.Skipper = DefaultBasicAuthConfig.Skipper
@ -72,7 +73,7 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
auth := c.Request().Header.Get(echo.HeaderAuthorization) auth := c.Request().Header.Get(echo.HeaderAuthorization)
l := len(basic) l := len(basic)
if len(auth) > l+1 && auth[:l] == basic { if len(auth) > l+1 && strings.ToLower(auth[:l]) == basic {
b, err := base64.StdEncoding.DecodeString(auth[l+1:]) b, err := base64.StdEncoding.DecodeString(auth[l+1:])
if err != nil { if err != nil {
return err return err
@ -87,6 +88,7 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
} else if valid { } else if valid {
return next(c) return next(c)
} }
break
} }
} }
} }

112
vendor/github.com/labstack/echo/middleware/body_dump.go generated vendored Normal file
View File

@ -0,0 +1,112 @@
package middleware
import (
"bufio"
"bytes"
"io/ioutil"
"net"
"net/http"
"io"
"github.com/labstack/echo"
)
type (
// BodyDumpConfig defines the config for BodyDump middleware.
BodyDumpConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// Handler receives request and response payload.
// Required.
Handler BodyDumpHandler
}
// BodyDumpHandler receives the request and response payload.
BodyDumpHandler func(echo.Context, []byte, []byte)
bodyDumpResponseWriter struct {
io.Writer
http.ResponseWriter
}
)
var (
// DefaultBodyDumpConfig is the default BodyDump middleware config.
DefaultBodyDumpConfig = BodyDumpConfig{
Skipper: DefaultSkipper,
}
)
// BodyDump returns a BodyDump middleware.
//
// BodyLimit middleware captures the request and response payload and calls the
// registered handler.
func BodyDump(handler BodyDumpHandler) echo.MiddlewareFunc {
c := DefaultBodyDumpConfig
c.Handler = handler
return BodyDumpWithConfig(c)
}
// BodyDumpWithConfig returns a BodyDump middleware with config.
// See: `BodyDump()`.
func BodyDumpWithConfig(config BodyDumpConfig) echo.MiddlewareFunc {
// Defaults
if config.Handler == nil {
panic("echo: body-dump middleware requires a handler function")
}
if config.Skipper == nil {
config.Skipper = DefaultBodyDumpConfig.Skipper
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) {
if config.Skipper(c) {
return next(c)
}
// Request
reqBody := []byte{}
if c.Request().Body != nil { // Read
reqBody, _ = ioutil.ReadAll(c.Request().Body)
}
c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) // Reset
// Response
resBody := new(bytes.Buffer)
mw := io.MultiWriter(c.Response().Writer, resBody)
writer := &bodyDumpResponseWriter{Writer: mw, ResponseWriter: c.Response().Writer}
c.Response().Writer = writer
if err = next(c); err != nil {
c.Error(err)
}
// Callback
config.Handler(c, reqBody, resBody.Bytes())
return
}
}
}
func (w *bodyDumpResponseWriter) WriteHeader(code int) {
w.ResponseWriter.WriteHeader(code)
}
func (w *bodyDumpResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
func (w *bodyDumpResponseWriter) Flush() {
w.ResponseWriter.(http.Flusher).Flush()
}
func (w *bodyDumpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return w.ResponseWriter.(http.Hijacker).Hijack()
}
func (w *bodyDumpResponseWriter) CloseNotify() <-chan bool {
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
}

View File

@ -17,7 +17,7 @@ type (
// Maximum allowed size for a request body, it can be specified // Maximum allowed size for a request body, it can be specified
// as `4x` or `4xB`, where x is one of the multiple from K, M, G, T or P. // as `4x` or `4xB`, where x is one of the multiple from K, M, G, T or P.
Limit string `json:"limit"` Limit string `yaml:"limit"`
limit int64 limit int64
} }
@ -30,7 +30,7 @@ type (
) )
var ( var (
// DefaultBodyLimitConfig is the default Gzip middleware config. // DefaultBodyLimitConfig is the default BodyLimit middleware config.
DefaultBodyLimitConfig = BodyLimitConfig{ DefaultBodyLimitConfig = BodyLimitConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
} }
@ -60,7 +60,7 @@ func BodyLimitWithConfig(config BodyLimitConfig) echo.MiddlewareFunc {
limit, err := bytes.Parse(config.Limit) limit, err := bytes.Parse(config.Limit)
if err != nil { if err != nil {
panic(fmt.Errorf("invalid body-limit=%s", config.Limit)) panic(fmt.Errorf("echo: invalid body-limit=%s", config.Limit))
} }
config.limit = limit config.limit = limit
pool := limitedReaderPool(config) pool := limitedReaderPool(config)

View File

@ -20,7 +20,7 @@ type (
// Gzip compression level. // Gzip compression level.
// Optional. Default value -1. // Optional. Default value -1.
Level int `json:"level"` Level int `yaml:"level"`
} }
gzipResponseWriter struct { gzipResponseWriter struct {
@ -67,7 +67,7 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
res := c.Response() res := c.Response()
res.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding) res.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding)
if strings.Contains(c.Request().Header.Get(echo.HeaderAcceptEncoding), gzipScheme) { if strings.Contains(c.Request().Header.Get(echo.HeaderAcceptEncoding), gzipScheme) {
res.Header().Add(echo.HeaderContentEncoding, gzipScheme) // Issue #806 res.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806
rw := res.Writer rw := res.Writer
w, err := gzip.NewWriterLevel(rw, config.Level) w, err := gzip.NewWriterLevel(rw, config.Level)
if err != nil { if err != nil {
@ -98,6 +98,7 @@ func (w *gzipResponseWriter) WriteHeader(code int) {
if code == http.StatusNoContent { // Issue #489 if code == http.StatusNoContent { // Issue #489
w.ResponseWriter.Header().Del(echo.HeaderContentEncoding) w.ResponseWriter.Header().Del(echo.HeaderContentEncoding)
} }
w.Header().Del(echo.HeaderContentLength) // Issue #444
w.ResponseWriter.WriteHeader(code) w.ResponseWriter.WriteHeader(code)
} }

View File

@ -16,34 +16,34 @@ type (
// AllowOrigin defines a list of origins that may access the resource. // AllowOrigin defines a list of origins that may access the resource.
// Optional. Default value []string{"*"}. // Optional. Default value []string{"*"}.
AllowOrigins []string `json:"allow_origins"` AllowOrigins []string `yaml:"allow_origins"`
// AllowMethods defines a list methods allowed when accessing the resource. // AllowMethods defines a list methods allowed when accessing the resource.
// This is used in response to a preflight request. // This is used in response to a preflight request.
// Optional. Default value DefaultCORSConfig.AllowMethods. // Optional. Default value DefaultCORSConfig.AllowMethods.
AllowMethods []string `json:"allow_methods"` AllowMethods []string `yaml:"allow_methods"`
// AllowHeaders defines a list of request headers that can be used when // AllowHeaders defines a list of request headers that can be used when
// making the actual request. This in response to a preflight request. // making the actual request. This in response to a preflight request.
// Optional. Default value []string{}. // Optional. Default value []string{}.
AllowHeaders []string `json:"allow_headers"` AllowHeaders []string `yaml:"allow_headers"`
// AllowCredentials indicates whether or not the response to the request // AllowCredentials indicates whether or not the response to the request
// can be exposed when the credentials flag is true. When used as part of // can be exposed when the credentials flag is true. When used as part of
// a response to a preflight request, this indicates whether or not the // a response to a preflight request, this indicates whether or not the
// actual request can be made using credentials. // actual request can be made using credentials.
// Optional. Default value false. // Optional. Default value false.
AllowCredentials bool `json:"allow_credentials"` AllowCredentials bool `yaml:"allow_credentials"`
// ExposeHeaders defines a whitelist headers that clients are allowed to // ExposeHeaders defines a whitelist headers that clients are allowed to
// access. // access.
// Optional. Default value []string{}. // Optional. Default value []string{}.
ExposeHeaders []string `json:"expose_headers"` ExposeHeaders []string `yaml:"expose_headers"`
// MaxAge indicates how long (in seconds) the results of a preflight request // MaxAge indicates how long (in seconds) the results of a preflight request
// can be cached. // can be cached.
// Optional. Default value 0. // Optional. Default value 0.
MaxAge int `json:"max_age"` MaxAge int `yaml:"max_age"`
} }
) )

View File

@ -18,7 +18,7 @@ type (
Skipper Skipper Skipper Skipper
// TokenLength is the length of the generated token. // TokenLength is the length of the generated token.
TokenLength uint8 `json:"token_length"` TokenLength uint8 `yaml:"token_length"`
// Optional. Default value 32. // Optional. Default value 32.
// TokenLookup is a string in the form of "<source>:<key>" that is used // TokenLookup is a string in the form of "<source>:<key>" that is used
@ -28,35 +28,35 @@ type (
// - "header:<name>" // - "header:<name>"
// - "form:<name>" // - "form:<name>"
// - "query:<name>" // - "query:<name>"
TokenLookup string `json:"token_lookup"` TokenLookup string `yaml:"token_lookup"`
// Context key to store generated CSRF token into context. // Context key to store generated CSRF token into context.
// Optional. Default value "csrf". // Optional. Default value "csrf".
ContextKey string `json:"context_key"` ContextKey string `yaml:"context_key"`
// Name of the CSRF cookie. This cookie will store CSRF token. // Name of the CSRF cookie. This cookie will store CSRF token.
// Optional. Default value "csrf". // Optional. Default value "csrf".
CookieName string `json:"cookie_name"` CookieName string `yaml:"cookie_name"`
// Domain of the CSRF cookie. // Domain of the CSRF cookie.
// Optional. Default value none. // Optional. Default value none.
CookieDomain string `json:"cookie_domain"` CookieDomain string `yaml:"cookie_domain"`
// Path of the CSRF cookie. // Path of the CSRF cookie.
// Optional. Default value none. // Optional. Default value none.
CookiePath string `json:"cookie_path"` CookiePath string `yaml:"cookie_path"`
// Max age (in seconds) of the CSRF cookie. // Max age (in seconds) of the CSRF cookie.
// Optional. Default value 86400 (24hr). // Optional. Default value 86400 (24hr).
CookieMaxAge int `json:"cookie_max_age"` CookieMaxAge int `yaml:"cookie_max_age"`
// Indicates if CSRF cookie is secure. // Indicates if CSRF cookie is secure.
// Optional. Default value false. // Optional. Default value false.
CookieSecure bool `json:"cookie_secure"` CookieSecure bool `yaml:"cookie_secure"`
// Indicates if CSRF cookie is HTTP only. // Indicates if CSRF cookie is HTTP only.
// Optional. Default value false. // Optional. Default value false.
CookieHTTPOnly bool `json:"cookie_http_only"` CookieHTTPOnly bool `yaml:"cookie_http_only"`
} }
// csrfTokenExtractor defines a function that takes `echo.Context` and returns // csrfTokenExtractor defines a function that takes `echo.Context` and returns

View File

@ -1,7 +1,6 @@
package middleware package middleware
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"reflect" "reflect"
@ -57,6 +56,12 @@ const (
AlgorithmHS256 = "HS256" AlgorithmHS256 = "HS256"
) )
// Errors
var (
ErrJWTMissing = echo.NewHTTPError(http.StatusBadRequest, "Missing or malformed jwt")
ErrJWTInvalid = echo.NewHTTPError(http.StatusUnauthorized, "Invalid or expired jwt")
)
var ( var (
// DefaultJWTConfig is the default JWT auth middleware config. // DefaultJWTConfig is the default JWT auth middleware config.
DefaultJWTConfig = JWTConfig{ DefaultJWTConfig = JWTConfig{
@ -77,7 +82,7 @@ var (
// //
// See: https://jwt.io/introduction // See: https://jwt.io/introduction
// See `JWTConfig.TokenLookup` // See `JWTConfig.TokenLookup`
func JWT(key []byte) echo.MiddlewareFunc { func JWT(key interface{}) echo.MiddlewareFunc {
c := DefaultJWTConfig c := DefaultJWTConfig
c.SigningKey = key c.SigningKey = key
return JWTWithConfig(c) return JWTWithConfig(c)
@ -134,14 +139,15 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
auth, err := extractor(c) auth, err := extractor(c)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error()) return err
} }
token := new(jwt.Token) token := new(jwt.Token)
// Issue #647, #656 // Issue #647, #656
if _, ok := config.Claims.(jwt.MapClaims); ok { if _, ok := config.Claims.(jwt.MapClaims); ok {
token, err = jwt.Parse(auth, config.keyFunc) token, err = jwt.Parse(auth, config.keyFunc)
} else { } else {
claims := reflect.ValueOf(config.Claims).Interface().(jwt.Claims) t := reflect.ValueOf(config.Claims).Type().Elem()
claims := reflect.New(t).Interface().(jwt.Claims)
token, err = jwt.ParseWithClaims(auth, claims, config.keyFunc) token, err = jwt.ParseWithClaims(auth, claims, config.keyFunc)
} }
if err == nil && token.Valid { if err == nil && token.Valid {
@ -149,7 +155,11 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
c.Set(config.ContextKey, token) c.Set(config.ContextKey, token)
return next(c) return next(c)
} }
return echo.ErrUnauthorized return &echo.HTTPError{
Code: ErrJWTInvalid.Code,
Message: ErrJWTInvalid.Message,
Inner: err,
}
} }
} }
} }
@ -162,7 +172,7 @@ func jwtFromHeader(header string, authScheme string) jwtExtractor {
if len(auth) > l+1 && auth[:l] == authScheme { if len(auth) > l+1 && auth[:l] == authScheme {
return auth[l+1:], nil return auth[l+1:], nil
} }
return "", errors.New("Missing or invalid jwt in the request header") return "", ErrJWTMissing
} }
} }
@ -171,7 +181,7 @@ func jwtFromQuery(param string) jwtExtractor {
return func(c echo.Context) (string, error) { return func(c echo.Context) (string, error) {
token := c.QueryParam(param) token := c.QueryParam(param)
if token == "" { if token == "" {
return "", errors.New("Missing jwt in the query string") return "", ErrJWTMissing
} }
return token, nil return token, nil
} }
@ -182,7 +192,7 @@ func jwtFromCookie(name string) jwtExtractor {
return func(c echo.Context) (string, error) { return func(c echo.Context) (string, error) {
cookie, err := c.Cookie(name) cookie, err := c.Cookie(name)
if err != nil { if err != nil {
return "", errors.New("Missing jwt in the cookie") return "", ErrJWTMissing
} }
return cookie.Value, nil return cookie.Value, nil
} }

View File

@ -20,7 +20,8 @@ type (
// Possible values: // Possible values:
// - "header:<name>" // - "header:<name>"
// - "query:<name>" // - "query:<name>"
KeyLookup string `json:"key_lookup"` // - "form:<name>"
KeyLookup string `yaml:"key_lookup"`
// AuthScheme to be used in the Authorization header. // AuthScheme to be used in the Authorization header.
// Optional. Default value "Bearer". // Optional. Default value "Bearer".
@ -72,7 +73,7 @@ func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {
config.KeyLookup = DefaultKeyAuthConfig.KeyLookup config.KeyLookup = DefaultKeyAuthConfig.KeyLookup
} }
if config.Validator == nil { if config.Validator == nil {
panic("key-auth middleware requires a validator function") panic("echo: key-auth middleware requires a validator function")
} }
// Initialize // Initialize
@ -81,6 +82,8 @@ func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {
switch parts[0] { switch parts[0] {
case "query": case "query":
extractor = keyFromQuery(parts[1]) extractor = keyFromQuery(parts[1])
case "form":
extractor = keyFromForm(parts[1])
} }
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
@ -134,3 +137,14 @@ func keyFromQuery(param string) keyExtractor {
return key, nil return key, nil
} }
} }
// keyFromForm returns a `keyExtractor` that extracts key from the form.
func keyFromForm(param string) keyExtractor {
return func(c echo.Context) (string, error) {
key := c.FormValue(param)
if key == "" {
return "", errors.New("Missing key in the form")
}
return key, nil
}
}

View File

@ -26,6 +26,7 @@ type (
// - time_unix_nano // - time_unix_nano
// - time_rfc3339 // - time_rfc3339
// - time_rfc3339_nano // - time_rfc3339_nano
// - time_custom
// - id (Request ID) // - id (Request ID)
// - remote_ip // - remote_ip
// - uri // - uri
@ -46,7 +47,10 @@ type (
// Example "${remote_ip} ${status}" // Example "${remote_ip} ${status}"
// //
// Optional. Default value DefaultLoggerConfig.Format. // Optional. Default value DefaultLoggerConfig.Format.
Format string `json:"format"` Format string `yaml:"format"`
// Optional. Default value DefaultLoggerConfig.CustomTimeFormat.
CustomTimeFormat string `yaml:"custom_time_format"`
// Output is a writer where logs in JSON format are written. // Output is a writer where logs in JSON format are written.
// Optional. Default value os.Stdout. // Optional. Default value os.Stdout.
@ -66,6 +70,7 @@ var (
`"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` + `"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` +
`"latency_human":"${latency_human}","bytes_in":${bytes_in},` + `"latency_human":"${latency_human}","bytes_in":${bytes_in},` +
`"bytes_out":${bytes_out}}` + "\n", `"bytes_out":${bytes_out}}` + "\n",
CustomTimeFormat:"2006-01-02 15:04:05.00000",
Output: os.Stdout, Output: os.Stdout,
colorer: color.New(), colorer: color.New(),
} }
@ -126,6 +131,8 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
return buf.WriteString(time.Now().Format(time.RFC3339)) return buf.WriteString(time.Now().Format(time.RFC3339))
case "time_rfc3339_nano": case "time_rfc3339_nano":
return buf.WriteString(time.Now().Format(time.RFC3339Nano)) return buf.WriteString(time.Now().Format(time.RFC3339Nano))
case "time_custom":
return buf.WriteString(time.Now().Format(config.CustomTimeFormat))
case "id": case "id":
id := req.Header.Get(echo.HeaderXRequestID) id := req.Header.Get(echo.HeaderXRequestID)
if id == "" { if id == "" {

View File

@ -1,6 +1,12 @@
package middleware package middleware
import "github.com/labstack/echo" import (
"regexp"
"strconv"
"strings"
"github.com/labstack/echo"
)
type ( type (
// Skipper defines a function to skip middleware. Returning true skips processing // Skipper defines a function to skip middleware. Returning true skips processing
@ -8,6 +14,21 @@ type (
Skipper func(c echo.Context) bool Skipper func(c echo.Context) bool
) )
func captureTokens(pattern *regexp.Regexp, input string) *strings.Replacer {
groups := pattern.FindAllStringSubmatch(input, -1)
if groups == nil {
return nil
}
values := groups[0][1:]
replace := make([]string, 2*len(values))
for i, v := range values {
j := 2 * i
replace[j] = "$" + strconv.Itoa(i+1)
replace[j+1] = v
}
return strings.NewReplacer(replace...)
}
// DefaultSkipper returns false which processes the middleware. // DefaultSkipper returns false which processes the middleware.
func DefaultSkipper(echo.Context) bool { func DefaultSkipper(echo.Context) bool {
return false return false

View File

@ -1,7 +1,6 @@
package middleware package middleware
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"math/rand" "math/rand"
@ -9,6 +8,9 @@ import (
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"regexp"
"strings"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -25,33 +27,56 @@ type (
// Balancer defines a load balancing technique. // Balancer defines a load balancing technique.
// Required. // Required.
// Possible values:
// - RandomBalancer
// - RoundRobinBalancer
Balancer ProxyBalancer Balancer ProxyBalancer
// Rewrite defines URL path rewrite rules. The values captured in asterisk can be
// retrieved by index e.g. $1, $2 and so on.
// Examples:
// "/old": "/new",
// "/api/*": "/$1",
// "/js/*": "/public/javascripts/$1",
// "/users/*/orders/*": "/user/$1/order/$2",
Rewrite map[string]string
rewriteRegex map[*regexp.Regexp]string
} }
// ProxyTarget defines the upstream target. // ProxyTarget defines the upstream target.
ProxyTarget struct { ProxyTarget struct {
URL *url.URL Name string
} URL *url.URL
// RandomBalancer implements a random load balancing technique.
RandomBalancer struct {
Targets []*ProxyTarget
random *rand.Rand
}
// RoundRobinBalancer implements a round-robin load balancing technique.
RoundRobinBalancer struct {
Targets []*ProxyTarget
i uint32
} }
// ProxyBalancer defines an interface to implement a load balancing technique. // ProxyBalancer defines an interface to implement a load balancing technique.
ProxyBalancer interface { ProxyBalancer interface {
AddTarget(*ProxyTarget) bool
RemoveTarget(string) bool
Next() *ProxyTarget Next() *ProxyTarget
} }
commonBalancer struct {
targets []*ProxyTarget
mutex sync.RWMutex
}
// RandomBalancer implements a random load balancing technique.
randomBalancer struct {
*commonBalancer
random *rand.Rand
}
// RoundRobinBalancer implements a round-robin load balancing technique.
roundRobinBalancer struct {
*commonBalancer
i uint32
}
)
var (
// DefaultProxyConfig is the default Proxy middleware config.
DefaultProxyConfig = ProxyConfig{
Skipper: DefaultSkipper,
}
) )
func proxyHTTP(t *ProxyTarget) http.Handler { func proxyHTTP(t *ProxyTarget) http.Handler {
@ -60,29 +85,25 @@ func proxyHTTP(t *ProxyTarget) http.Handler {
func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler { func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h, ok := w.(http.Hijacker) in, _, err := c.Response().Hijack()
if !ok {
c.Error(errors.New("proxy raw, not a hijacker"))
return
}
in, _, err := h.Hijack()
if err != nil { if err != nil {
c.Error(fmt.Errorf("proxy raw, hijack error=%v, url=%s", r.URL, err)) c.Error(fmt.Errorf("proxy raw, hijack error=%v, url=%s", t.URL, err))
return return
} }
defer in.Close() defer in.Close()
out, err := net.Dial("tcp", t.URL.Host) out, err := net.Dial("tcp", t.URL.Host)
if err != nil { if err != nil {
he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, dial error=%v, url=%s", r.URL, err)) he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, dial error=%v, url=%s", t.URL, err))
c.Error(he) c.Error(he)
return return
} }
defer out.Close() defer out.Close()
// Write header
err = r.Write(out) err = r.Write(out)
if err != nil { if err != nil {
he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, request copy error=%v, url=%s", r.URL, err)) he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, request header copy error=%v, url=%s", t.URL, err))
c.Error(he) c.Error(he)
return return
} }
@ -97,29 +118,81 @@ func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
go cp(in, out) go cp(in, out)
err = <-errc err = <-errc
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
c.Logger().Errorf("proxy raw, error=%v, url=%s", r.URL, err) c.Logger().Errorf("proxy raw, copy body error=%v, url=%s", t.URL, err)
} }
}) })
} }
// Next randomly returns an upstream target. // NewRandomBalancer returns a random proxy balancer.
func (r *RandomBalancer) Next() *ProxyTarget { func NewRandomBalancer(targets []*ProxyTarget) ProxyBalancer {
if r.random == nil { b := &randomBalancer{commonBalancer: new(commonBalancer)}
r.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) b.targets = targets
return b
}
// NewRoundRobinBalancer returns a round-robin proxy balancer.
func NewRoundRobinBalancer(targets []*ProxyTarget) ProxyBalancer {
b := &roundRobinBalancer{commonBalancer: new(commonBalancer)}
b.targets = targets
return b
}
// AddTarget adds an upstream target to the list.
func (b *commonBalancer) AddTarget(target *ProxyTarget) bool {
for _, t := range b.targets {
if t.Name == target.Name {
return false
}
} }
return r.Targets[r.random.Intn(len(r.Targets))] b.mutex.Lock()
defer b.mutex.Unlock()
b.targets = append(b.targets, target)
return true
}
// RemoveTarget removes an upstream target from the list.
func (b *commonBalancer) RemoveTarget(name string) bool {
b.mutex.Lock()
defer b.mutex.Unlock()
for i, t := range b.targets {
if t.Name == name {
b.targets = append(b.targets[:i], b.targets[i+1:]...)
return true
}
}
return false
}
// Next randomly returns an upstream target.
func (b *randomBalancer) Next() *ProxyTarget {
if b.random == nil {
b.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
}
b.mutex.RLock()
defer b.mutex.RUnlock()
return b.targets[b.random.Intn(len(b.targets))]
} }
// Next returns an upstream target using round-robin technique. // Next returns an upstream target using round-robin technique.
func (r *RoundRobinBalancer) Next() *ProxyTarget { func (b *roundRobinBalancer) Next() *ProxyTarget {
r.i = r.i % uint32(len(r.Targets)) b.i = b.i % uint32(len(b.targets))
t := r.Targets[r.i] t := b.targets[b.i]
atomic.AddUint32(&r.i, 1) atomic.AddUint32(&b.i, 1)
return t return t
} }
// Proxy returns an HTTP/WebSocket reverse proxy middleware. // Proxy returns a Proxy middleware.
func Proxy(config ProxyConfig) echo.MiddlewareFunc { //
// Proxy middleware forwards the request to upstream server using a configured load balancing technique.
func Proxy(balancer ProxyBalancer) echo.MiddlewareFunc {
c := DefaultProxyConfig
c.Balancer = balancer
return ProxyWithConfig(c)
}
// ProxyWithConfig returns a Proxy middleware with config.
// See: `Proxy()`
func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
// Defaults // Defaults
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultLoggerConfig.Skipper config.Skipper = DefaultLoggerConfig.Skipper
@ -127,13 +200,32 @@ func Proxy(config ProxyConfig) echo.MiddlewareFunc {
if config.Balancer == nil { if config.Balancer == nil {
panic("echo: proxy middleware requires balancer") panic("echo: proxy middleware requires balancer")
} }
config.rewriteRegex = map[*regexp.Regexp]string{}
// Initialize
for k, v := range config.Rewrite {
k = strings.Replace(k, "*", "(\\S*)", -1)
config.rewriteRegex[regexp.MustCompile(k)] = v
}
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) { return func(c echo.Context) (err error) {
if config.Skipper(c) {
return next(c)
}
req := c.Request() req := c.Request()
res := c.Response() res := c.Response()
tgt := config.Balancer.Next() tgt := config.Balancer.Next()
// Rewrite
for k, v := range config.rewriteRegex {
replacer := captureTokens(k, req.URL.Path)
if replacer != nil {
req.URL.Path = replacer.Replace(v)
}
}
// Fix header // Fix header
if req.Header.Get(echo.HeaderXRealIP) == "" { if req.Header.Get(echo.HeaderXRealIP) == "" {
req.Header.Set(echo.HeaderXRealIP, c.RealIP()) req.Header.Set(echo.HeaderXRealIP, c.RealIP())

View File

@ -5,7 +5,6 @@ import (
"runtime" "runtime"
"github.com/labstack/echo" "github.com/labstack/echo"
"github.com/labstack/gommon/color"
) )
type ( type (
@ -16,16 +15,16 @@ type (
// Size of the stack to be printed. // Size of the stack to be printed.
// Optional. Default value 4KB. // Optional. Default value 4KB.
StackSize int `json:"stack_size"` StackSize int `yaml:"stack_size"`
// DisableStackAll disables formatting stack traces of all other goroutines // DisableStackAll disables formatting stack traces of all other goroutines
// into buffer after the trace for the current goroutine. // into buffer after the trace for the current goroutine.
// Optional. Default value false. // Optional. Default value false.
DisableStackAll bool `json:"disable_stack_all"` DisableStackAll bool `yaml:"disable_stack_all"`
// DisablePrintStack disables printing stack trace. // DisablePrintStack disables printing stack trace.
// Optional. Default value as false. // Optional. Default value as false.
DisablePrintStack bool `json:"disable_print_stack"` DisablePrintStack bool `yaml:"disable_print_stack"`
} }
) )
@ -64,17 +63,14 @@ func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
var err error err, ok := r.(error)
switch r := r.(type) { if !ok {
case error:
err = r
default:
err = fmt.Errorf("%v", r) err = fmt.Errorf("%v", r)
} }
stack := make([]byte, config.StackSize) stack := make([]byte, config.StackSize)
length := runtime.Stack(stack, !config.DisableStackAll) length := runtime.Stack(stack, !config.DisableStackAll)
if !config.DisablePrintStack { if !config.DisablePrintStack {
c.Logger().Printf("[%s] %s %s\n", color.Red("PANIC RECOVER"), err, stack[:length]) c.Logger().Printf("[PANIC RECOVER] %v %s\n", err, stack[:length])
} }
c.Error(err) c.Error(err)
} }

View File

@ -6,29 +6,28 @@ import (
"github.com/labstack/echo" "github.com/labstack/echo"
) )
type ( // RedirectConfig defines the config for Redirect middleware.
// RedirectConfig defines the config for Redirect middleware. type RedirectConfig struct {
RedirectConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper
Skipper Skipper
// Status code to be used when redirecting the request. // Status code to be used when redirecting the request.
// Optional. Default value http.StatusMovedPermanently. // Optional. Default value http.StatusMovedPermanently.
Code int `json:"code"` Code int `yaml:"code"`
} }
)
const ( // redirectLogic represents a function that given a scheme, host and uri
www = "www" // can both: 1) determine if redirect is needed (will set ok accordingly) and
) // 2) return the appropriate redirect url.
type redirectLogic func(scheme, host, uri string) (ok bool, url string)
var ( const www = "www"
// DefaultRedirectConfig is the default Redirect middleware config.
DefaultRedirectConfig = RedirectConfig{ // DefaultRedirectConfig is the default Redirect middleware config.
Skipper: DefaultSkipper, var DefaultRedirectConfig = RedirectConfig{
Code: http.StatusMovedPermanently, Skipper: DefaultSkipper,
} Code: http.StatusMovedPermanently,
) }
// HTTPSRedirect redirects http requests to https. // HTTPSRedirect redirects http requests to https.
// For example, http://labstack.com will be redirect to https://labstack.com. // For example, http://labstack.com will be redirect to https://labstack.com.
@ -41,29 +40,12 @@ func HTTPSRedirect() echo.MiddlewareFunc {
// HTTPSRedirectWithConfig returns an HTTPSRedirect middleware with config. // HTTPSRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `HTTPSRedirect()`. // See `HTTPSRedirect()`.
func HTTPSRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func HTTPSRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
// Defaults return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if config.Skipper == nil { if ok = scheme != "https"; ok {
config.Skipper = DefaultTrailingSlashConfig.Skipper url = "https://" + host + uri
}
if config.Code == 0 {
config.Code = DefaultRedirectConfig.Code
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
host := req.Host
uri := req.RequestURI
if !c.IsTLS() {
return c.Redirect(config.Code, "https://"+host+uri)
}
return next(c)
} }
} return
})
} }
// HTTPSWWWRedirect redirects http requests to https www. // HTTPSWWWRedirect redirects http requests to https www.
@ -77,29 +59,12 @@ func HTTPSWWWRedirect() echo.MiddlewareFunc {
// HTTPSWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // HTTPSWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `HTTPSWWWRedirect()`. // See `HTTPSWWWRedirect()`.
func HTTPSWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func HTTPSWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
// Defaults return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if config.Skipper == nil { if ok = scheme != "https" && host[:3] != www; ok {
config.Skipper = DefaultTrailingSlashConfig.Skipper url = "https://www." + host + uri
}
if config.Code == 0 {
config.Code = DefaultRedirectConfig.Code
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
host := req.Host
uri := req.RequestURI
if !c.IsTLS() && host[:3] != www {
return c.Redirect(config.Code, "https://www."+host+uri)
}
return next(c)
} }
} return
})
} }
// HTTPSNonWWWRedirect redirects http requests to https non www. // HTTPSNonWWWRedirect redirects http requests to https non www.
@ -113,32 +78,15 @@ func HTTPSNonWWWRedirect() echo.MiddlewareFunc {
// HTTPSNonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // HTTPSNonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `HTTPSNonWWWRedirect()`. // See `HTTPSNonWWWRedirect()`.
func HTTPSNonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func HTTPSNonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
// Defaults return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if config.Skipper == nil { if ok = scheme != "https"; ok {
config.Skipper = DefaultTrailingSlashConfig.Skipper if host[:3] == www {
} host = host[4:]
if config.Code == 0 {
config.Code = DefaultRedirectConfig.Code
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
} }
url = "https://" + host + uri
req := c.Request()
host := req.Host
uri := req.RequestURI
if !c.IsTLS() {
if host[:3] == www {
return c.Redirect(config.Code, "https://"+host[4:]+uri)
}
return c.Redirect(config.Code, "https://"+host+uri)
}
return next(c)
} }
} return
})
} }
// WWWRedirect redirects non www requests to www. // WWWRedirect redirects non www requests to www.
@ -152,30 +100,12 @@ func WWWRedirect() echo.MiddlewareFunc {
// WWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // WWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `WWWRedirect()`. // See `WWWRedirect()`.
func WWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func WWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
// Defaults return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if config.Skipper == nil { if ok = host[:3] != www; ok {
config.Skipper = DefaultTrailingSlashConfig.Skipper url = scheme + "://www." + host + uri
}
if config.Code == 0 {
config.Code = DefaultRedirectConfig.Code
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
scheme := c.Scheme()
host := req.Host
if host[:3] != www {
uri := req.RequestURI
return c.Redirect(config.Code, scheme+"://www."+host+uri)
}
return next(c)
} }
} return
})
} }
// NonWWWRedirect redirects www requests to non www. // NonWWWRedirect redirects www requests to non www.
@ -189,6 +119,15 @@ func NonWWWRedirect() echo.MiddlewareFunc {
// NonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // NonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `NonWWWRedirect()`. // See `NonWWWRedirect()`.
func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if ok = host[:3] == www; ok {
url = scheme + "://" + host[4:] + uri
}
return
})
}
func redirect(config RedirectConfig, cb redirectLogic) echo.MiddlewareFunc {
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultTrailingSlashConfig.Skipper config.Skipper = DefaultTrailingSlashConfig.Skipper
} }
@ -202,13 +141,12 @@ func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
return next(c) return next(c)
} }
req := c.Request() req, scheme := c.Request(), c.Scheme()
scheme := c.Scheme()
host := req.Host host := req.Host
if host[:3] == www { if ok, url := cb(scheme, host, req.RequestURI); ok {
uri := req.RequestURI return c.Redirect(config.Code, url)
return c.Redirect(config.Code, scheme+"://"+host[4:]+uri)
} }
return next(c) return next(c)
} }
} }

83
vendor/github.com/labstack/echo/middleware/rewrite.go generated vendored Normal file
View File

@ -0,0 +1,83 @@
package middleware
import (
"regexp"
"strings"
"github.com/labstack/echo"
)
type (
// RewriteConfig defines the config for Rewrite middleware.
RewriteConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// Rules defines the URL path rewrite rules. The values captured in asterisk can be
// retrieved by index e.g. $1, $2 and so on.
// Example:
// "/old": "/new",
// "/api/*": "/$1",
// "/js/*": "/public/javascripts/$1",
// "/users/*/orders/*": "/user/$1/order/$2",
// Required.
Rules map[string]string `yaml:"rules"`
rulesRegex map[*regexp.Regexp]string
}
)
var (
// DefaultRewriteConfig is the default Rewrite middleware config.
DefaultRewriteConfig = RewriteConfig{
Skipper: DefaultSkipper,
}
)
// Rewrite returns a Rewrite middleware.
//
// Rewrite middleware rewrites the URL path based on the provided rules.
func Rewrite(rules map[string]string) echo.MiddlewareFunc {
c := DefaultRewriteConfig
c.Rules = rules
return RewriteWithConfig(c)
}
// RewriteWithConfig returns a Rewrite middleware with config.
// See: `Rewrite()`.
func RewriteWithConfig(config RewriteConfig) echo.MiddlewareFunc {
// Defaults
if config.Rules == nil {
panic("echo: rewrite middleware requires url path rewrite rules")
}
if config.Skipper == nil {
config.Skipper = DefaultBodyDumpConfig.Skipper
}
config.rulesRegex = map[*regexp.Regexp]string{}
// Initialize
for k, v := range config.Rules {
k = strings.Replace(k, "*", "(\\S*)", -1)
config.rulesRegex[regexp.MustCompile(k)] = v
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
// Rewrite
for k, v := range config.rulesRegex {
replacer := captureTokens(k, req.URL.Path)
if replacer != nil {
req.URL.Path = replacer.Replace(v)
}
}
return next(c)
}
}
}

View File

@ -15,12 +15,12 @@ type (
// XSSProtection provides protection against cross-site scripting attack (XSS) // XSSProtection provides protection against cross-site scripting attack (XSS)
// by setting the `X-XSS-Protection` header. // by setting the `X-XSS-Protection` header.
// Optional. Default value "1; mode=block". // Optional. Default value "1; mode=block".
XSSProtection string `json:"xss_protection"` XSSProtection string `yaml:"xss_protection"`
// ContentTypeNosniff provides protection against overriding Content-Type // ContentTypeNosniff provides protection against overriding Content-Type
// header by setting the `X-Content-Type-Options` header. // header by setting the `X-Content-Type-Options` header.
// Optional. Default value "nosniff". // Optional. Default value "nosniff".
ContentTypeNosniff string `json:"content_type_nosniff"` ContentTypeNosniff string `yaml:"content_type_nosniff"`
// XFrameOptions can be used to indicate whether or not a browser should // XFrameOptions can be used to indicate whether or not a browser should
// be allowed to render a page in a <frame>, <iframe> or <object> . // be allowed to render a page in a <frame>, <iframe> or <object> .
@ -32,27 +32,27 @@ type (
// - "SAMEORIGIN" - The page can only be displayed in a frame on the same origin as the page itself. // - "SAMEORIGIN" - The page can only be displayed in a frame on the same origin as the page itself.
// - "DENY" - The page cannot be displayed in a frame, regardless of the site attempting to do so. // - "DENY" - The page cannot be displayed in a frame, regardless of the site attempting to do so.
// - "ALLOW-FROM uri" - The page can only be displayed in a frame on the specified origin. // - "ALLOW-FROM uri" - The page can only be displayed in a frame on the specified origin.
XFrameOptions string `json:"x_frame_options"` XFrameOptions string `yaml:"x_frame_options"`
// HSTSMaxAge sets the `Strict-Transport-Security` header to indicate how // HSTSMaxAge sets the `Strict-Transport-Security` header to indicate how
// long (in seconds) browsers should remember that this site is only to // long (in seconds) browsers should remember that this site is only to
// be accessed using HTTPS. This reduces your exposure to some SSL-stripping // be accessed using HTTPS. This reduces your exposure to some SSL-stripping
// man-in-the-middle (MITM) attacks. // man-in-the-middle (MITM) attacks.
// Optional. Default value 0. // Optional. Default value 0.
HSTSMaxAge int `json:"hsts_max_age"` HSTSMaxAge int `yaml:"hsts_max_age"`
// HSTSExcludeSubdomains won't include subdomains tag in the `Strict Transport Security` // HSTSExcludeSubdomains won't include subdomains tag in the `Strict Transport Security`
// header, excluding all subdomains from security policy. It has no effect // header, excluding all subdomains from security policy. It has no effect
// unless HSTSMaxAge is set to a non-zero value. // unless HSTSMaxAge is set to a non-zero value.
// Optional. Default value false. // Optional. Default value false.
HSTSExcludeSubdomains bool `json:"hsts_exclude_subdomains"` HSTSExcludeSubdomains bool `yaml:"hsts_exclude_subdomains"`
// ContentSecurityPolicy sets the `Content-Security-Policy` header providing // ContentSecurityPolicy sets the `Content-Security-Policy` header providing
// security against cross-site scripting (XSS), clickjacking and other code // security against cross-site scripting (XSS), clickjacking and other code
// injection attacks resulting from execution of malicious content in the // injection attacks resulting from execution of malicious content in the
// trusted web page context. // trusted web page context.
// Optional. Default value "". // Optional. Default value "".
ContentSecurityPolicy string `json:"content_security_policy"` ContentSecurityPolicy string `yaml:"content_security_policy"`
} }
) )

View File

@ -12,7 +12,7 @@ type (
// Status code to be used when redirecting the request. // Status code to be used when redirecting the request.
// Optional, but when provided the request is redirected using this code. // Optional, but when provided the request is redirected using this code.
RedirectCode int `json:"redirect_code"` RedirectCode int `yaml:"redirect_code"`
} }
) )

View File

@ -2,6 +2,7 @@ package middleware
import ( import (
"fmt" "fmt"
"net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -18,20 +19,20 @@ type (
// Root directory from where the static content is served. // Root directory from where the static content is served.
// Required. // Required.
Root string `json:"root"` Root string `yaml:"root"`
// Index file for serving a directory. // Index file for serving a directory.
// Optional. Default value "index.html". // Optional. Default value "index.html".
Index string `json:"index"` Index string `yaml:"index"`
// Enable HTML5 mode by forwarding all not-found requests to root so that // Enable HTML5 mode by forwarding all not-found requests to root so that
// SPA (single-page application) can handle the routing. // SPA (single-page application) can handle the routing.
// Optional. Default value false. // Optional. Default value false.
HTML5 bool `json:"html5"` HTML5 bool `yaml:"html5"`
// Enable directory browsing. // Enable directory browsing.
// Optional. Default value false. // Optional. Default value false.
Browse bool `json:"browse"` Browse bool `yaml:"browse"`
} }
) )
@ -66,7 +67,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
} }
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) (err error) {
if config.Skipper(c) { if config.Skipper(c) {
return next(c) return next(c)
} }
@ -75,17 +76,25 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`. if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`.
p = c.Param("*") p = c.Param("*")
} }
p, err = echo.PathUnescape(p)
if err != nil {
return
}
name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security
fi, err := os.Stat(name) fi, err := os.Stat(name)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
if config.HTML5 && path.Ext(p) == "" { if err = next(c); err != nil {
return c.File(filepath.Join(config.Root, config.Index)) if he, ok := err.(*echo.HTTPError); ok {
if config.HTML5 && he.Code == http.StatusNotFound {
return c.File(filepath.Join(config.Root, config.Index))
}
}
return
} }
return next(c)
} }
return err return
} }
if fi.IsDir() { if fi.IsDir() {
@ -99,7 +108,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return next(c) return next(c)
} }
return err return
} }
return c.File(index) return c.File(index)
@ -110,20 +119,20 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
} }
} }
func listDir(name string, res *echo.Response) error { func listDir(name string, res *echo.Response) (err error) {
dir, err := os.Open(name) dir, err := os.Open(name)
if err != nil { if err != nil {
return err return
} }
dirs, err := dir.Readdir(-1) dirs, err := dir.Readdir(-1)
if err != nil { if err != nil {
return err return
} }
// Create a directory index // Create a directory index
res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8)
if _, err = fmt.Fprintf(res, "<pre>\n"); err != nil { if _, err = fmt.Fprintf(res, "<pre>\n"); err != nil {
return err return
} }
for _, d := range dirs { for _, d := range dirs {
name := d.Name() name := d.Name()
@ -133,9 +142,9 @@ func listDir(name string, res *echo.Response) error {
name += "/" name += "/"
} }
if _, err = fmt.Fprintf(res, "<a href=\"%s\" style=\"color: %s;\">%s</a>\n", name, color, name); err != nil { if _, err = fmt.Fprintf(res, "<a href=\"%s\" style=\"color: %s;\">%s</a>\n", name, color, name); err != nil {
return err return
} }
} }
_, err = fmt.Fprintf(res, "</pre>\n") _, err = fmt.Fprintf(res, "</pre>\n")
return err return
} }

View File

@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"net" "net"
"net/http" "net/http"
"strconv"
) )
type ( type (
@ -11,11 +12,14 @@ type (
// by an HTTP handler to construct an HTTP response. // by an HTTP handler to construct an HTTP response.
// See: https://golang.org/pkg/net/http/#ResponseWriter // See: https://golang.org/pkg/net/http/#ResponseWriter
Response struct { Response struct {
Writer http.ResponseWriter echo *Echo
Status int contentLength int64
Size int64 beforeFuncs []func()
Committed bool afterFuncs []func()
echo *Echo Writer http.ResponseWriter
Status int
Size int64
Committed bool
} }
) )
@ -34,6 +38,17 @@ func (r *Response) Header() http.Header {
return r.Writer.Header() return r.Writer.Header()
} }
// Before registers a function which is called just before the response is written.
func (r *Response) Before(fn func()) {
r.beforeFuncs = append(r.beforeFuncs, fn)
}
// After registers a function which is called just after the response is written.
// If the `Content-Length` is unknown, none of the after function is executed.
func (r *Response) After(fn func()) {
r.afterFuncs = append(r.afterFuncs, fn)
}
// WriteHeader sends an HTTP response header with status code. If WriteHeader is // WriteHeader sends an HTTP response header with status code. If WriteHeader is
// not called explicitly, the first call to Write will trigger an implicit // not called explicitly, the first call to Write will trigger an implicit
// WriteHeader(http.StatusOK). Thus explicit calls to WriteHeader are mainly // WriteHeader(http.StatusOK). Thus explicit calls to WriteHeader are mainly
@ -43,9 +58,13 @@ func (r *Response) WriteHeader(code int) {
r.echo.Logger.Warn("response already committed") r.echo.Logger.Warn("response already committed")
return return
} }
for _, fn := range r.beforeFuncs {
fn()
}
r.Status = code r.Status = code
r.Writer.WriteHeader(code) r.Writer.WriteHeader(code)
r.Committed = true r.Committed = true
r.contentLength, _ = strconv.ParseInt(r.Header().Get(HeaderContentLength), 10, 0)
} }
// Write writes the data to the connection as part of an HTTP reply. // Write writes the data to the connection as part of an HTTP reply.
@ -55,6 +74,11 @@ func (r *Response) Write(b []byte) (n int, err error) {
} }
n, err = r.Writer.Write(b) n, err = r.Writer.Write(b)
r.Size += int64(n) r.Size += int64(n)
if r.Size == r.contentLength {
for _, fn := range r.afterFuncs {
fn()
}
}
return return
} }
@ -82,6 +106,9 @@ func (r *Response) CloseNotify() <-chan bool {
} }
func (r *Response) reset(w http.ResponseWriter) { func (r *Response) reset(w http.ResponseWriter) {
r.contentLength = 0
r.beforeFuncs = nil
r.afterFuncs = nil
r.Writer = w r.Writer = w
r.Size = 0 r.Size = 0
r.Status = http.StatusOK r.Status = http.StatusOK

View File

@ -1,7 +1,5 @@
package echo package echo
import "strings"
type ( type (
// Router is the registry of all registered routes for an `Echo` instance for // Router is the registry of all registered routes for an `Echo` instance for
// request matching and URL path parameter parsing. // request matching and URL path parameter parsing.
@ -175,12 +173,6 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
if len(cn.pnames) == 0 { // Issue #729 if len(cn.pnames) == 0 { // Issue #729
cn.pnames = pnames cn.pnames = pnames
} }
for i, n := range pnames {
// Param name aliases
if i < len(cn.pnames) && !strings.Contains(cn.pnames[i], n) {
cn.pnames[i] += "," + n
}
}
} }
} }
return return
@ -394,7 +386,7 @@ func (r *Router) Find(method, path string, c Context) {
if cn = cn.findChildByKind(akind); cn == nil { if cn = cn.findChildByKind(akind); cn == nil {
if nn != nil { if nn != nil {
cn = nn cn = nn
nn = nil // Next nn = cn.parent // Next (Issue #954)
search = ns search = ns
if nk == pkind { if nk == pkind {
goto Param goto Param

12
vendor/github.com/labstack/echo/util_go17.go generated vendored Normal file
View File

@ -0,0 +1,12 @@
// +build go1.7, !go1.8
package echo
import (
"net/url"
)
// PathUnescape is wraps `url.QueryUnescape`
func PathUnescape(s string) (string, error) {
return url.QueryUnescape(s)
}

10
vendor/github.com/labstack/echo/util_go18.go generated vendored Normal file
View File

@ -0,0 +1,10 @@
// +build go1.8
package echo
import "net/url"
// PathUnescape is wraps `url.PathUnescape`
func PathUnescape(s string) (string, error) {
return url.PathUnescape(s)
}

View File

@ -16,64 +16,62 @@ func (c *Client) registerBuiltins() {
c.Handlers.mu.Lock() c.Handlers.mu.Lock()
// Built-in things that should always be supported. // Built-in things that should always be supported.
c.Handlers.register(true, RPL_WELCOME, HandlerFunc(func(c *Client, e Event) { c.Handlers.register(true, true, RPL_WELCOME, HandlerFunc(handleConnect))
go handleConnect(c, e) c.Handlers.register(true, false, PING, HandlerFunc(handlePING))
})) c.Handlers.register(true, false, PONG, HandlerFunc(handlePONG))
c.Handlers.register(true, PING, HandlerFunc(handlePING))
c.Handlers.register(true, PONG, HandlerFunc(handlePONG))
if !c.Config.disableTracking { if !c.Config.disableTracking {
// Joins/parts/anything that may add/remove/rename users. // Joins/parts/anything that may add/remove/rename users.
c.Handlers.register(true, JOIN, HandlerFunc(handleJOIN)) c.Handlers.register(true, false, JOIN, HandlerFunc(handleJOIN))
c.Handlers.register(true, PART, HandlerFunc(handlePART)) c.Handlers.register(true, false, PART, HandlerFunc(handlePART))
c.Handlers.register(true, KICK, HandlerFunc(handleKICK)) c.Handlers.register(true, false, KICK, HandlerFunc(handleKICK))
c.Handlers.register(true, QUIT, HandlerFunc(handleQUIT)) c.Handlers.register(true, false, QUIT, HandlerFunc(handleQUIT))
c.Handlers.register(true, NICK, HandlerFunc(handleNICK)) c.Handlers.register(true, false, NICK, HandlerFunc(handleNICK))
c.Handlers.register(true, RPL_NAMREPLY, HandlerFunc(handleNAMES)) c.Handlers.register(true, false, RPL_NAMREPLY, HandlerFunc(handleNAMES))
// Modes. // Modes.
c.Handlers.register(true, MODE, HandlerFunc(handleMODE)) c.Handlers.register(true, false, MODE, HandlerFunc(handleMODE))
c.Handlers.register(true, RPL_CHANNELMODEIS, HandlerFunc(handleMODE)) c.Handlers.register(true, false, RPL_CHANNELMODEIS, HandlerFunc(handleMODE))
// WHO/WHOX responses. // WHO/WHOX responses.
c.Handlers.register(true, RPL_WHOREPLY, HandlerFunc(handleWHO)) c.Handlers.register(true, false, RPL_WHOREPLY, HandlerFunc(handleWHO))
c.Handlers.register(true, RPL_WHOSPCRPL, HandlerFunc(handleWHO)) c.Handlers.register(true, false, RPL_WHOSPCRPL, HandlerFunc(handleWHO))
// Other misc. useful stuff. // Other misc. useful stuff.
c.Handlers.register(true, TOPIC, HandlerFunc(handleTOPIC)) c.Handlers.register(true, false, TOPIC, HandlerFunc(handleTOPIC))
c.Handlers.register(true, RPL_TOPIC, HandlerFunc(handleTOPIC)) c.Handlers.register(true, false, RPL_TOPIC, HandlerFunc(handleTOPIC))
c.Handlers.register(true, RPL_MYINFO, HandlerFunc(handleMYINFO)) c.Handlers.register(true, false, RPL_MYINFO, HandlerFunc(handleMYINFO))
c.Handlers.register(true, RPL_ISUPPORT, HandlerFunc(handleISUPPORT)) c.Handlers.register(true, false, RPL_ISUPPORT, HandlerFunc(handleISUPPORT))
c.Handlers.register(true, RPL_MOTDSTART, HandlerFunc(handleMOTD)) c.Handlers.register(true, false, RPL_MOTDSTART, HandlerFunc(handleMOTD))
c.Handlers.register(true, RPL_MOTD, HandlerFunc(handleMOTD)) c.Handlers.register(true, false, RPL_MOTD, HandlerFunc(handleMOTD))
// Keep users lastactive times up to date. // Keep users lastactive times up to date.
c.Handlers.register(true, PRIVMSG, HandlerFunc(updateLastActive)) c.Handlers.register(true, false, PRIVMSG, HandlerFunc(updateLastActive))
c.Handlers.register(true, NOTICE, HandlerFunc(updateLastActive)) c.Handlers.register(true, false, NOTICE, HandlerFunc(updateLastActive))
c.Handlers.register(true, TOPIC, HandlerFunc(updateLastActive)) c.Handlers.register(true, false, TOPIC, HandlerFunc(updateLastActive))
c.Handlers.register(true, KICK, HandlerFunc(updateLastActive)) c.Handlers.register(true, false, KICK, HandlerFunc(updateLastActive))
// CAP IRCv3-specific tracking and functionality. // CAP IRCv3-specific tracking and functionality.
c.Handlers.register(true, CAP, HandlerFunc(handleCAP)) c.Handlers.register(true, false, CAP, HandlerFunc(handleCAP))
c.Handlers.register(true, CAP_CHGHOST, HandlerFunc(handleCHGHOST)) c.Handlers.register(true, false, CAP_CHGHOST, HandlerFunc(handleCHGHOST))
c.Handlers.register(true, CAP_AWAY, HandlerFunc(handleAWAY)) c.Handlers.register(true, false, CAP_AWAY, HandlerFunc(handleAWAY))
c.Handlers.register(true, CAP_ACCOUNT, HandlerFunc(handleACCOUNT)) c.Handlers.register(true, false, CAP_ACCOUNT, HandlerFunc(handleACCOUNT))
c.Handlers.register(true, ALL_EVENTS, HandlerFunc(handleTags)) c.Handlers.register(true, false, ALL_EVENTS, HandlerFunc(handleTags))
// SASL IRCv3 support. // SASL IRCv3 support.
c.Handlers.register(true, AUTHENTICATE, HandlerFunc(handleSASL)) c.Handlers.register(true, false, AUTHENTICATE, HandlerFunc(handleSASL))
c.Handlers.register(true, RPL_SASLSUCCESS, HandlerFunc(handleSASL)) c.Handlers.register(true, false, RPL_SASLSUCCESS, HandlerFunc(handleSASL))
c.Handlers.register(true, RPL_NICKLOCKED, HandlerFunc(handleSASLError)) c.Handlers.register(true, false, RPL_NICKLOCKED, HandlerFunc(handleSASLError))
c.Handlers.register(true, ERR_SASLFAIL, HandlerFunc(handleSASLError)) c.Handlers.register(true, false, ERR_SASLFAIL, HandlerFunc(handleSASLError))
c.Handlers.register(true, ERR_SASLTOOLONG, HandlerFunc(handleSASLError)) c.Handlers.register(true, false, ERR_SASLTOOLONG, HandlerFunc(handleSASLError))
c.Handlers.register(true, ERR_SASLABORTED, HandlerFunc(handleSASLError)) c.Handlers.register(true, false, ERR_SASLABORTED, HandlerFunc(handleSASLError))
c.Handlers.register(true, RPL_SASLMECHS, HandlerFunc(handleSASLError)) c.Handlers.register(true, false, RPL_SASLMECHS, HandlerFunc(handleSASLError))
} }
// Nickname collisions. // Nickname collisions.
c.Handlers.register(true, ERR_NICKNAMEINUSE, HandlerFunc(nickCollisionHandler)) c.Handlers.register(true, false, ERR_NICKNAMEINUSE, HandlerFunc(nickCollisionHandler))
c.Handlers.register(true, ERR_NICKCOLLISION, HandlerFunc(nickCollisionHandler)) c.Handlers.register(true, false, ERR_NICKCOLLISION, HandlerFunc(nickCollisionHandler))
c.Handlers.register(true, ERR_UNAVAILRESOURCE, HandlerFunc(nickCollisionHandler)) c.Handlers.register(true, false, ERR_UNAVAILRESOURCE, HandlerFunc(nickCollisionHandler))
c.Handlers.mu.Unlock() c.Handlers.mu.Unlock()
} }
@ -389,7 +387,7 @@ func handleISUPPORT(c *Client, e Event) {
c.state.Lock() c.state.Lock()
// Skip the first parameter, as it's our nickname. // Skip the first parameter, as it's our nickname.
for i := 1; i < len(e.Params); i++ { for i := 1; i < len(e.Params); i++ {
j := strings.IndexByte(e.Params[i], 0x3D) // = j := strings.IndexByte(e.Params[i], '=')
if j < 1 || (j+1) == len(e.Params[i]) { if j < 1 || (j+1) == len(e.Params[i]) {
c.state.serverOptions[e.Params[i]] = "" c.state.serverOptions[e.Params[i]] = ""

View File

@ -136,7 +136,7 @@ func handleCAP(c *Client, e Event) {
} }
// Let them know which ones we'd like to enable. // Let them know which ones we'd like to enable.
c.write(&Event{Command: CAP, Params: []string{CAP_REQ}, Trailing: strings.Join(c.state.tmpCap, " ")}) c.write(&Event{Command: CAP, Params: []string{CAP_REQ}, Trailing: strings.Join(c.state.tmpCap, " "), EmptyTrailing: true})
// Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests // Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests
// due to cap-notify, we can re-evaluate what we can support. // due to cap-notify, we can re-evaluate what we can support.
@ -375,11 +375,11 @@ func handleTags(c *Client, e Event) {
} }
const ( const (
prefixTag byte = 0x40 // @ prefixTag byte = '@'
prefixTagValue byte = 0x3D // = prefixTagValue byte = '='
prefixUserTag byte = 0x2B // + prefixUserTag byte = '+'
tagSeparator byte = 0x3B // ; tagSeparator byte = ';'
maxTagLength int = 511 // 510 + @ and " " (space), though space usually not included. maxTagLength int = 511 // 510 + @ and " " (space), though space usually not included.
) )
// Tags represents the key-value pairs in IRCv3 message tags. The map contains // Tags represents the key-value pairs in IRCv3 message tags. The map contains
@ -618,7 +618,7 @@ func validTag(name string) bool {
for i := 0; i < len(name); i++ { for i := 0; i < len(name); i++ {
// A-Z, a-z, 0-9, -/._ // A-Z, a-z, 0-9, -/._
if (name[i] < 0x41 || name[i] > 0x5A) && (name[i] < 0x61 || name[i] > 0x7A) && (name[i] < 0x2D || name[i] > 0x39) && name[i] != 0x5F { if (name[i] < 'A' || name[i] > 'Z') && (name[i] < 'a' || name[i] > 'z') && (name[i] < '-' || name[i] > '9') && name[i] != '_' {
return false return false
} }
} }
@ -631,7 +631,7 @@ func validTag(name string) bool {
func validTagValue(value string) bool { func validTagValue(value string) bool {
for i := 0; i < len(value); i++ { for i := 0; i < len(value); i++ {
// Don't allow any invisible chars within the tag, or semicolons. // Don't allow any invisible chars within the tag, or semicolons.
if value[i] < 0x21 || value[i] > 0x7E || value[i] == 0x3B { if value[i] < '!' || value[i] > '~' || value[i] == ';' {
return false return false
} }
} }

View File

@ -191,18 +191,6 @@ func (conf *Config) isValid() error {
// connected. // connected.
var ErrNotConnected = errors.New("client is not connected to server") var ErrNotConnected = errors.New("client is not connected to server")
// ErrDisconnected is called when Config.Retries is less than 1, and we
// non-intentionally disconnected from the server.
var ErrDisconnected = errors.New("unexpectedly disconnected")
// ErrInvalidTarget should be returned if the target which you are
// attempting to send an event to is invalid or doesn't match RFC spec.
type ErrInvalidTarget struct {
Target string
}
func (e *ErrInvalidTarget) Error() string { return "invalid target: " + e.Target }
// New creates a new IRC client with the specified server, name and config. // New creates a new IRC client with the specified server, name and config.
func New(config Config) *Client { func New(config Config) *Client {
c := &Client{ c := &Client{
@ -253,6 +241,37 @@ func (c *Client) String() string {
) )
} }
// TLSConnectionState returns the TLS connection state from tls.Conn{}, which
// is useful to return needed TLS fingerprint info, certificates, verify cert
// expiration dates, etc. Will only return an error if the underlying
// connection wasn't established using TLS (see ErrConnNotTLS), or if the
// client isn't connected.
func (c *Client) TLSConnectionState() (*tls.ConnectionState, error) {
c.mu.RLock()
defer c.mu.RUnlock()
if c.conn == nil {
return nil, ErrNotConnected
}
c.conn.mu.RLock()
defer c.conn.mu.RUnlock()
if !c.conn.connected {
return nil, ErrNotConnected
}
if tlsConn, ok := c.conn.sock.(*tls.Conn); ok {
cs := tlsConn.ConnectionState()
return &cs, nil
}
return nil, ErrConnNotTLS
}
// ErrConnNotTLS is returned when Client.TLSConnectionState() is called, and
// the connection to the server wasn't made with TLS.
var ErrConnNotTLS = errors.New("underlying connection is not tls")
// Close closes the network connection to the server, and sends a STOPPED // Close closes the network connection to the server, and sends a STOPPED
// event. This should cause Connect() to return with nil. This should be // event. This should cause Connect() to return with nil. This should be
// safe to call multiple times. See Connect()'s documentation on how // safe to call multiple times. See Connect()'s documentation on how
@ -387,7 +406,7 @@ func (c *Client) ConnSince() (since *time.Duration, err error) {
} }
// IsConnected returns true if the client is connected to the server. // IsConnected returns true if the client is connected to the server.
func (c *Client) IsConnected() (connected bool) { func (c *Client) IsConnected() bool {
c.mu.RLock() c.mu.RLock()
if c.conn == nil { if c.conn == nil {
c.mu.RUnlock() c.mu.RUnlock()
@ -395,7 +414,7 @@ func (c *Client) IsConnected() (connected bool) {
} }
c.conn.mu.RLock() c.conn.mu.RLock()
connected = c.conn.connected connected := c.conn.connected
c.conn.mu.RUnlock() c.conn.mu.RUnlock()
c.mu.RUnlock() c.mu.RUnlock()
@ -445,9 +464,9 @@ func (c *Client) GetHost() string {
return c.state.host return c.state.host
} }
// Channels returns the active list of channels that the client is in. // ChannelList returns the active list of channel names that the client is in.
// Panics if tracking is disabled. // Panics if tracking is disabled.
func (c *Client) Channels() []string { func (c *Client) ChannelList() []string {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.RLock() c.state.RLock()
@ -463,9 +482,26 @@ func (c *Client) Channels() []string {
return channels return channels
} }
// Users returns the active list of users that the client is tracking across // Channels returns the active channels that the client is in. Panics if
// all files. Panics if tracking is disabled. // tracking is disabled.
func (c *Client) Users() []string { func (c *Client) Channels() []*Channel {
c.panicIfNotTracking()
c.state.RLock()
channels := make([]*Channel, len(c.state.channels))
var i int
for channel := range c.state.channels {
channels[i] = c.state.channels[channel].Copy()
i++
}
c.state.RUnlock()
return channels
}
// UserList returns the active list of nicknames that the client is tracking
// across all networks. Panics if tracking is disabled.
func (c *Client) UserList() []string {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.RLock() c.state.RLock()
@ -481,6 +517,23 @@ func (c *Client) Users() []string {
return users return users
} }
// Users returns the active users that the client is tracking across all
// networks. Panics if tracking is disabled.
func (c *Client) Users() []*User {
c.panicIfNotTracking()
c.state.RLock()
users := make([]*User, len(c.state.users))
var i int
for user := range c.state.users {
users[i] = c.state.users[user].Copy()
i++
}
c.state.RUnlock()
return users
}
// LookupChannel looks up a given channel in state. If the channel doesn't // LookupChannel looks up a given channel in state. If the channel doesn't
// exist, nil is returned. Panics if tracking is disabled. // exist, nil is returned. Panics if tracking is disabled.
func (c *Client) LookupChannel(name string) *Channel { func (c *Client) LookupChannel(name string) *Channel {
@ -562,30 +615,30 @@ func (c *Client) NetworkName() (name string) {
// supplied this information during connection. May be empty if the server // supplied this information during connection. May be empty if the server
// does not support RPL_MYINFO. Will panic if used when tracking has been // does not support RPL_MYINFO. Will panic if used when tracking has been
// disabled. // disabled.
func (c *Client) ServerVersion() (version string) { func (c *Client) ServerVersion() string {
c.panicIfNotTracking() c.panicIfNotTracking()
version, _ = c.GetServerOption("VERSION") version, _ := c.GetServerOption("VERSION")
return version return version
} }
// ServerMOTD returns the servers message of the day, if the server has sent // ServerMOTD returns the servers message of the day, if the server has sent
// it upon connect. Will panic if used when tracking has been disabled. // it upon connect. Will panic if used when tracking has been disabled.
func (c *Client) ServerMOTD() (motd string) { func (c *Client) ServerMOTD() string {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.RLock() c.state.RLock()
motd = c.state.motd motd := c.state.motd
c.state.RUnlock() c.state.RUnlock()
return motd return motd
} }
// Lag is the latency between the server and the client. This is measured by // Latency is the latency between the server and the client. This is measured
// determining the difference in time between when we ping the server, and // by determining the difference in time between when we ping the server, and
// when we receive a pong. // when we receive a pong.
func (c *Client) Lag() time.Duration { func (c *Client) Latency() time.Duration {
c.mu.RLock() c.mu.RLock()
c.conn.mu.RLock() c.conn.mu.RLock()
delta := c.conn.lastPong.Sub(c.conn.lastPing) delta := c.conn.lastPong.Sub(c.conn.lastPing)

View File

@ -12,8 +12,9 @@ import (
// Input is a wrapper for events, based around private messages. // Input is a wrapper for events, based around private messages.
type Input struct { type Input struct {
Origin *girc.Event Origin *girc.Event
Args []string Args []string
RawArgs string
} }
// Command is an IRC command, supporting aliases, help documentation and easy // Command is an IRC command, supporting aliases, help documentation and easy
@ -189,8 +190,9 @@ func (ch *CmdHandler) Execute(client *girc.Client, event girc.Event) {
} }
in := &Input{ in := &Input{
Origin: &event, Origin: &event,
Args: args, Args: args,
RawArgs: parsed[2],
} }
go cmd.Fn(client, in) go cmd.Fn(client, in)

View File

@ -16,18 +16,13 @@ type Commands struct {
} }
// Nick changes the client nickname. // Nick changes the client nickname.
func (cmd *Commands) Nick(name string) error { func (cmd *Commands) Nick(name string) {
if !IsValidNick(name) {
return &ErrInvalidTarget{Target: name}
}
cmd.c.Send(&Event{Command: NICK, Params: []string{name}}) cmd.c.Send(&Event{Command: NICK, Params: []string{name}})
return nil
} }
// Join attempts to enter a list of IRC channels, at bulk if possible to // Join attempts to enter a list of IRC channels, at bulk if possible to
// prevent sending extensive JOIN commands. // prevent sending extensive JOIN commands.
func (cmd *Commands) Join(channels ...string) error { func (cmd *Commands) Join(channels ...string) {
// We can join multiple channels at once, however we need to ensure that // We can join multiple channels at once, however we need to ensure that
// we are not exceeding the line length. (see maxLength) // we are not exceeding the line length. (see maxLength)
max := maxLength - len(JOIN) - 1 max := maxLength - len(JOIN) - 1
@ -35,10 +30,6 @@ func (cmd *Commands) Join(channels ...string) error {
var buffer string var buffer string
for i := 0; i < len(channels); i++ { for i := 0; i < len(channels); i++ {
if !IsValidChannel(channels[i]) {
return &ErrInvalidTarget{Target: channels[i]}
}
if len(buffer+","+channels[i]) > max { if len(buffer+","+channels[i]) > max {
cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}}) cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
buffer = "" buffer = ""
@ -53,91 +44,74 @@ func (cmd *Commands) Join(channels ...string) error {
if i == len(channels)-1 { if i == len(channels)-1 {
cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}}) cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
return nil return
} }
} }
return nil
} }
// JoinKey attempts to enter an IRC channel with a password. // JoinKey attempts to enter an IRC channel with a password.
func (cmd *Commands) JoinKey(channel, password string) error { func (cmd *Commands) JoinKey(channel, password string) {
if !IsValidChannel(channel) {
return &ErrInvalidTarget{Target: channel}
}
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel, password}}) cmd.c.Send(&Event{Command: JOIN, Params: []string{channel, password}})
return nil
} }
// Part leaves an IRC channel. // Part leaves an IRC channel.
func (cmd *Commands) Part(channel, message string) error { func (cmd *Commands) Part(channels ...string) {
if !IsValidChannel(channel) { for i := 0; i < len(channels); i++ {
return &ErrInvalidTarget{Target: channel} cmd.c.Send(&Event{Command: PART, Params: []string{channels[i]}})
} }
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel}})
return nil
} }
// PartMessage leaves an IRC channel with a specified leave message. // PartMessage leaves an IRC channel with a specified leave message.
func (cmd *Commands) PartMessage(channel, message string) error { func (cmd *Commands) PartMessage(channel, message string) {
if !IsValidChannel(channel) { cmd.c.Send(&Event{Command: PART, Params: []string{channel}, Trailing: message, EmptyTrailing: true})
return &ErrInvalidTarget{Target: channel}
}
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel}, Trailing: message})
return nil
} }
// SendCTCP sends a CTCP request to target. Note that this method uses // SendCTCP sends a CTCP request to target. Note that this method uses
// PRIVMSG specifically. // PRIVMSG specifically. ctcpType is the CTCP command, e.g. "FINGER", "TIME",
func (cmd *Commands) SendCTCP(target, ctcpType, message string) error { // "VERSION", etc.
func (cmd *Commands) SendCTCP(target, ctcpType, message string) {
out := encodeCTCPRaw(ctcpType, message) out := encodeCTCPRaw(ctcpType, message)
if out == "" { if out == "" {
return errors.New("invalid CTCP") panic(fmt.Sprintf("invalid CTCP: %s -> %s: %s", target, ctcpType, message))
} }
return cmd.Message(target, out) cmd.Message(target, out)
} }
// SendCTCPf sends a CTCP request to target using a specific format. Note that // SendCTCPf sends a CTCP request to target using a specific format. Note that
// this method uses PRIVMSG specifically. // this method uses PRIVMSG specifically. ctcpType is the CTCP command, e.g.
func (cmd *Commands) SendCTCPf(target, ctcpType, format string, a ...interface{}) error { // "FINGER", "TIME", "VERSION", etc.
return cmd.SendCTCP(target, ctcpType, fmt.Sprintf(format, a...)) func (cmd *Commands) SendCTCPf(target, ctcpType, format string, a ...interface{}) {
cmd.SendCTCP(target, ctcpType, fmt.Sprintf(format, a...))
} }
// SendCTCPReplyf sends a CTCP response to target using a specific format. // SendCTCPReplyf sends a CTCP response to target using a specific format.
// Note that this method uses NOTICE specifically. // Note that this method uses NOTICE specifically. ctcpType is the CTCP
func (cmd *Commands) SendCTCPReplyf(target, ctcpType, format string, a ...interface{}) error { // command, e.g. "FINGER", "TIME", "VERSION", etc.
return cmd.SendCTCPReply(target, ctcpType, fmt.Sprintf(format, a...)) func (cmd *Commands) SendCTCPReplyf(target, ctcpType, format string, a ...interface{}) {
cmd.SendCTCPReply(target, ctcpType, fmt.Sprintf(format, a...))
} }
// SendCTCPReply sends a CTCP response to target. Note that this method uses // SendCTCPReply sends a CTCP response to target. Note that this method uses
// NOTICE specifically. // NOTICE specifically.
func (cmd *Commands) SendCTCPReply(target, ctcpType, message string) error { func (cmd *Commands) SendCTCPReply(target, ctcpType, message string) {
out := encodeCTCPRaw(ctcpType, message) out := encodeCTCPRaw(ctcpType, message)
if out == "" { if out == "" {
return errors.New("invalid CTCP") panic(fmt.Sprintf("invalid CTCP: %s -> %s: %s", target, ctcpType, message))
} }
return cmd.Notice(target, out) cmd.Notice(target, out)
} }
// Message sends a PRIVMSG to target (either channel, service, or user). // Message sends a PRIVMSG to target (either channel, service, or user).
func (cmd *Commands) Message(target, message string) error { func (cmd *Commands) Message(target, message string) {
if !IsValidNick(target) && !IsValidChannel(target) { cmd.c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message, EmptyTrailing: true})
return &ErrInvalidTarget{Target: target}
}
cmd.c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message})
return nil
} }
// Messagef sends a formated PRIVMSG to target (either channel, service, or // Messagef sends a formated PRIVMSG to target (either channel, service, or
// user). // user).
func (cmd *Commands) Messagef(target, format string, a ...interface{}) error { func (cmd *Commands) Messagef(target, format string, a ...interface{}) {
return cmd.Message(target, fmt.Sprintf(format, a...)) cmd.Message(target, fmt.Sprintf(format, a...))
} }
// ErrInvalidSource is returned when a method needs to know the origin of an // ErrInvalidSource is returned when a method needs to know the origin of an
@ -146,94 +120,95 @@ func (cmd *Commands) Messagef(target, format string, a ...interface{}) error {
var ErrInvalidSource = errors.New("event has nil or invalid source address") var ErrInvalidSource = errors.New("event has nil or invalid source address")
// Reply sends a reply to channel or user, based on where the supplied event // Reply sends a reply to channel or user, based on where the supplied event
// originated from. See also ReplyTo(). // originated from. See also ReplyTo(). Panics if the incoming event has no
func (cmd *Commands) Reply(event Event, message string) error { // source.
func (cmd *Commands) Reply(event Event, message string) {
if event.Source == nil { if event.Source == nil {
return ErrInvalidSource panic(ErrInvalidSource)
} }
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) { if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
return cmd.Message(event.Params[0], message) cmd.Message(event.Params[0], message)
return
} }
return cmd.Message(event.Source.Name, message) cmd.Message(event.Source.Name, message)
} }
// Replyf sends a reply to channel or user with a format string, based on // Replyf sends a reply to channel or user with a format string, based on
// where the supplied event originated from. See also ReplyTof(). // where the supplied event originated from. See also ReplyTof(). Panics if
func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) error { // the incoming event has no source.
return cmd.Reply(event, fmt.Sprintf(format, a...)) func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) {
cmd.Reply(event, fmt.Sprintf(format, a...))
} }
// ReplyTo sends a reply to a channel or user, based on where the supplied // ReplyTo sends a reply to a channel or user, based on where the supplied
// event originated from. ReplyTo(), when originating from a channel will // event originated from. ReplyTo(), when originating from a channel will
// default to replying with "<user>, <message>". See also Reply(). // default to replying with "<user>, <message>". See also Reply(). Panics if
func (cmd *Commands) ReplyTo(event Event, message string) error { // the incoming event has no source.
func (cmd *Commands) ReplyTo(event Event, message string) {
if event.Source == nil { if event.Source == nil {
return ErrInvalidSource panic(ErrInvalidSource)
} }
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) { if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
return cmd.Message(event.Params[0], event.Source.Name+", "+message) cmd.Message(event.Params[0], event.Source.Name+", "+message)
return
} }
return cmd.Message(event.Source.Name, message) cmd.Message(event.Source.Name, message)
} }
// ReplyTof sends a reply to a channel or user with a format string, based // ReplyTof sends a reply to a channel or user with a format string, based
// on where the supplied event originated from. ReplyTo(), when originating // on where the supplied event originated from. ReplyTo(), when originating
// from a channel will default to replying with "<user>, <message>". See // from a channel will default to replying with "<user>, <message>". See
// also Replyf(). // also Replyf(). Panics if the incoming event has no source.
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) error { func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) {
return cmd.ReplyTo(event, fmt.Sprintf(format, a...)) cmd.ReplyTo(event, fmt.Sprintf(format, a...))
} }
// Action sends a PRIVMSG ACTION (/me) to target (either channel, service, // Action sends a PRIVMSG ACTION (/me) to target (either channel, service,
// or user). // or user).
func (cmd *Commands) Action(target, message string) error { func (cmd *Commands) Action(target, message string) {
if !IsValidNick(target) && !IsValidChannel(target) {
return &ErrInvalidTarget{Target: target}
}
cmd.c.Send(&Event{ cmd.c.Send(&Event{
Command: PRIVMSG, Command: PRIVMSG,
Params: []string{target}, Params: []string{target},
Trailing: fmt.Sprintf("\001ACTION %s\001", message), Trailing: fmt.Sprintf("\001ACTION %s\001", message),
}) })
return nil
} }
// Actionf sends a formated PRIVMSG ACTION (/me) to target (either channel, // Actionf sends a formated PRIVMSG ACTION (/me) to target (either channel,
// service, or user). // service, or user).
func (cmd *Commands) Actionf(target, format string, a ...interface{}) error { func (cmd *Commands) Actionf(target, format string, a ...interface{}) {
return cmd.Action(target, fmt.Sprintf(format, a...)) cmd.Action(target, fmt.Sprintf(format, a...))
} }
// Notice sends a NOTICE to target (either channel, service, or user). // Notice sends a NOTICE to target (either channel, service, or user).
func (cmd *Commands) Notice(target, message string) error { func (cmd *Commands) Notice(target, message string) {
if !IsValidNick(target) && !IsValidChannel(target) { cmd.c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message, EmptyTrailing: true})
return &ErrInvalidTarget{Target: target}
}
cmd.c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message})
return nil
} }
// Noticef sends a formated NOTICE to target (either channel, service, or // Noticef sends a formated NOTICE to target (either channel, service, or
// user). // user).
func (cmd *Commands) Noticef(target, format string, a ...interface{}) error { func (cmd *Commands) Noticef(target, format string, a ...interface{}) {
return cmd.Notice(target, fmt.Sprintf(format, a...)) cmd.Notice(target, fmt.Sprintf(format, a...))
} }
// SendRaw sends a raw string back to the server, without carriage returns // SendRaw sends a raw string (or multiple) to the server, without carriage
// or newlines. // returns or newlines. Returns an error if one of the raw strings cannot be
func (cmd *Commands) SendRaw(raw string) error { // properly parsed.
e := ParseEvent(raw) func (cmd *Commands) SendRaw(raw ...string) error {
if e == nil { var event *Event
return errors.New("invalid event: " + raw)
for i := 0; i < len(raw); i++ {
event = ParseEvent(raw[i])
if event == nil {
return errors.New("invalid event: " + raw[i])
}
cmd.c.Send(event)
} }
cmd.c.Send(e)
return nil return nil
} }
@ -246,31 +221,26 @@ func (cmd *Commands) SendRawf(format string, a ...interface{}) error {
// Topic sets the topic of channel to message. Does not verify the length // Topic sets the topic of channel to message. Does not verify the length
// of the topic. // of the topic.
func (cmd *Commands) Topic(channel, message string) { func (cmd *Commands) Topic(channel, message string) {
cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel}, Trailing: message}) cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel}, Trailing: message, EmptyTrailing: true})
} }
// Who sends a WHO query to the server, which will attempt WHOX by default. // Who sends a WHO query to the server, which will attempt WHOX by default.
// See http://faerion.sourceforge.net/doc/irc/whox.var for more details. This // See http://faerion.sourceforge.net/doc/irc/whox.var for more details. This
// sends "%tcuhnr,2" per default. Do not use "1" as this will conflict with // sends "%tcuhnr,2" per default. Do not use "1" as this will conflict with
// girc's builtin tracking functionality. // girc's builtin tracking functionality.
func (cmd *Commands) Who(target string) error { func (cmd *Commands) Who(users ...string) {
if !IsValidNick(target) && !IsValidChannel(target) && !IsValidUser(target) { for i := 0; i < len(users); i++ {
return &ErrInvalidTarget{Target: target} cmd.c.Send(&Event{Command: WHO, Params: []string{users[i], "%tcuhnr,2"}})
} }
cmd.c.Send(&Event{Command: WHO, Params: []string{target, "%tcuhnr,2"}})
return nil
} }
// Whois sends a WHOIS query to the server, targeted at a specific user. // Whois sends a WHOIS query to the server, targeted at a specific user (or
// as WHOIS is a bit slower, you may want to use WHO for brief user info. // set of users). As WHOIS is a bit slower, you may want to use WHO for brief
func (cmd *Commands) Whois(nick string) error { // user info.
if !IsValidNick(nick) { func (cmd *Commands) Whois(users ...string) {
return &ErrInvalidTarget{Target: nick} for i := 0; i < len(users); i++ {
cmd.c.Send(&Event{Command: WHOIS, Params: []string{users[i]}})
} }
cmd.c.Send(&Event{Command: WHOIS, Params: []string{nick}})
return nil
} }
// Ping sends a PING query to the server, with a specific identifier that // Ping sends a PING query to the server, with a specific identifier that
@ -294,36 +264,40 @@ func (cmd *Commands) Oper(user, pass string) {
// Kick sends a KICK query to the server, attempting to kick nick from // Kick sends a KICK query to the server, attempting to kick nick from
// channel, with reason. If reason is blank, one will not be sent to the // channel, with reason. If reason is blank, one will not be sent to the
// server. // server.
func (cmd *Commands) Kick(channel, nick, reason string) error { func (cmd *Commands) Kick(channel, user, reason string) {
if !IsValidChannel(channel) {
return &ErrInvalidTarget{Target: channel}
}
if !IsValidNick(nick) {
return &ErrInvalidTarget{Target: nick}
}
if reason != "" { if reason != "" {
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}, Trailing: reason}) cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user}, Trailing: reason, EmptyTrailing: true})
return nil
} }
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}}) cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user}})
return nil }
// Ban adds the +b mode on the given mask on a channel.
func (cmd *Commands) Ban(channel, mask string) {
cmd.Mode(channel, "+b", mask)
}
// Unban removes the +b mode on the given mask on a channel.
func (cmd *Commands) Unban(channel, mask string) {
cmd.Mode(channel, "-b", mask)
}
// Mode sends a mode change to the server which should be applied to target
// (usually a channel or user), along with a set of modes (generally "+m",
// "+mmmm", or "-m", where "m" is the mode you want to change). Params is only
// needed if the mode change requires a parameter (ban or invite-only exclude.)
func (cmd *Commands) Mode(target, modes string, params ...string) {
out := []string{target, modes}
out = append(out, params...)
cmd.c.Send(&Event{Command: MODE, Params: out})
} }
// Invite sends a INVITE query to the server, to invite nick to channel. // Invite sends a INVITE query to the server, to invite nick to channel.
func (cmd *Commands) Invite(channel, nick string) error { func (cmd *Commands) Invite(channel string, users ...string) {
if !IsValidChannel(channel) { for i := 0; i < len(users); i++ {
return &ErrInvalidTarget{Target: channel} cmd.c.Send(&Event{Command: INVITE, Params: []string{users[i], channel}})
} }
if !IsValidNick(nick) {
return &ErrInvalidTarget{Target: nick}
}
cmd.c.Send(&Event{Command: INVITE, Params: []string{nick, channel}})
return nil
} }
// Away sends a AWAY query to the server, suggesting that the client is no // Away sends a AWAY query to the server, suggesting that the client is no
@ -348,10 +322,10 @@ func (cmd *Commands) Back() {
// Supports multiple channels at once, in hopes it will reduce extensive // Supports multiple channels at once, in hopes it will reduce extensive
// LIST queries to the server. Supply no channels to run a list against the // LIST queries to the server. Supply no channels to run a list against the
// entire server (warning, that may mean LOTS of channels!) // entire server (warning, that may mean LOTS of channels!)
func (cmd *Commands) List(channels ...string) error { func (cmd *Commands) List(channels ...string) {
if len(channels) == 0 { if len(channels) == 0 {
cmd.c.Send(&Event{Command: LIST}) cmd.c.Send(&Event{Command: LIST})
return nil return
} }
// We can LIST multiple channels at once, however we need to ensure that // We can LIST multiple channels at once, however we need to ensure that
@ -361,10 +335,6 @@ func (cmd *Commands) List(channels ...string) error {
var buffer string var buffer string
for i := 0; i < len(channels); i++ { for i := 0; i < len(channels); i++ {
if !IsValidChannel(channels[i]) {
return &ErrInvalidTarget{Target: channels[i]}
}
if len(buffer+","+channels[i]) > max { if len(buffer+","+channels[i]) > max {
cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}}) cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
buffer = "" buffer = ""
@ -379,20 +349,13 @@ func (cmd *Commands) List(channels ...string) error {
if i == len(channels)-1 { if i == len(channels)-1 {
cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}}) cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
return nil return
} }
} }
return nil
} }
// Whowas sends a WHOWAS query to the server. amount is the amount of results // Whowas sends a WHOWAS query to the server. amount is the amount of results
// you want back. // you want back.
func (cmd *Commands) Whowas(nick string, amount int) error { func (cmd *Commands) Whowas(user string, amount int) {
if !IsValidNick(nick) { cmd.c.Send(&Event{Command: WHOWAS, Params: []string{user, string(amount)}})
return &ErrInvalidTarget{Target: nick}
}
cmd.c.Send(&Event{Command: WHOWAS, Params: []string{nick, string(amount)}})
return nil
} }

View File

@ -58,7 +58,7 @@ func decodeCTCP(e *Event) *CTCPEvent {
if s < 0 { if s < 0 {
for i := 0; i < len(text); i++ { for i := 0; i < len(text); i++ {
// Check for A-Z, 0-9. // Check for A-Z, 0-9.
if (text[i] < 0x41 || text[i] > 0x5A) && (text[i] < 0x30 || text[i] > 0x39) { if (text[i] < 'A' || text[i] > 'Z') && (text[i] < '0' || text[i] > '9') {
return nil return nil
} }
} }
@ -74,7 +74,7 @@ func decodeCTCP(e *Event) *CTCPEvent {
// Loop through checking the tag first. // Loop through checking the tag first.
for i := 0; i < s; i++ { for i := 0; i < s; i++ {
// Check for A-Z, 0-9. // Check for A-Z, 0-9.
if (text[i] < 0x41 || text[i] > 0x5A) && (text[i] < 0x30 || text[i] > 0x39) { if (text[i] < 'A' || text[i] > 'Z') && (text[i] < '0' || text[i] > '9') {
return nil return nil
} }
} }
@ -168,7 +168,7 @@ func (c *CTCP) parseCMD(cmd string) string {
for i := 0; i < len(cmd); i++ { for i := 0; i < len(cmd); i++ {
// Check for A-Z, 0-9. // Check for A-Z, 0-9.
if (cmd[i] < 0x41 || cmd[i] > 0x5A) && (cmd[i] < 0x30 || cmd[i] > 0x39) { if (cmd[i] < 'A' || cmd[i] > 'Z') && (cmd[i] < '0' || cmd[i] > '9') {
return "" return ""
} }
} }

View File

@ -11,8 +11,8 @@ import (
) )
const ( const (
eventSpace byte = 0x20 // Separator. eventSpace byte = ' ' // Separator.
maxLength = 510 // Maximum length is 510 (2 for line endings). maxLength = 510 // Maximum length is 510 (2 for line endings).
) )
// cutCRFunc is used to trim CR characters from prefixes/messages. // cutCRFunc is used to trim CR characters from prefixes/messages.
@ -256,7 +256,7 @@ func (e *Event) Bytes() []byte {
// Strip newlines and carriage returns. // Strip newlines and carriage returns.
for i := 0; i < len(out); i++ { for i := 0; i < len(out); i++ {
if out[i] == 0x0A || out[i] == 0x0D { if out[i] == '\n' || out[i] == '\r' {
out = append(out[:i], out[i+1:]...) out = append(out[:i], out[i+1:]...)
i-- // Decrease the index so we can pick up where we left off. i-- // Decrease the index so we can pick up where we left off.
} }
@ -432,9 +432,9 @@ func (e *Event) StripAction() string {
} }
const ( const (
messagePrefix byte = 0x3A // ":" -- prefix or last argument messagePrefix byte = ':' // Prefix or last argument.
prefixIdent byte = 0x21 // "!" -- username prefixIdent byte = '!' // Username.
prefixHost byte = 0x40 // "@" -- hostname prefixHost byte = '@' // Hostname.
) )
// Source represents the sender of an IRC event, see RFC1459 section 2.3.1. // Source represents the sender of an IRC event, see RFC1459 section 2.3.1.

View File

@ -12,8 +12,8 @@ import (
) )
const ( const (
fmtOpenChar = 0x7B // { fmtOpenChar = '{'
fmtCloseChar = 0x7D // } fmtCloseChar = '}'
) )
var fmtColors = map[string]int{ var fmtColors = map[string]int{
@ -113,7 +113,7 @@ func Fmt(text string) string {
if last > -1 { if last > -1 {
// A-Z, a-z, and "," // A-Z, a-z, and ","
if text[i] != 0x2c && (text[i] <= 0x41 || text[i] >= 0x5a) && (text[i] <= 0x61 || text[i] >= 0x7a) { if text[i] != ',' && (text[i] <= 'A' || text[i] >= 'Z') && (text[i] <= 'a' || text[i] >= 'z') {
last = -1 last = -1
continue continue
} }
@ -127,10 +127,10 @@ func Fmt(text string) string {
// See Fmt() for more information. // See Fmt() for more information.
func TrimFmt(text string) string { func TrimFmt(text string) string {
for color := range fmtColors { for color := range fmtColors {
text = strings.Replace(text, "{"+color+"}", "", -1) text = strings.Replace(text, string(fmtOpenChar)+color+string(fmtCloseChar), "", -1)
} }
for code := range fmtCodes { for code := range fmtCodes {
text = strings.Replace(text, "{"+code+"}", "", -1) text = strings.Replace(text, string(fmtOpenChar)+code+string(fmtCloseChar), "", -1)
} }
return text return text
@ -175,9 +175,10 @@ func IsValidChannel(channel string) bool {
return false return false
} }
// #, +, !<channelid>, or & // #, +, !<channelid>, ~, or &
// Including "*" in the prefix list, as this is commonly used (e.g. ZNC) // Including "*" and "~" in the prefix list, as these are commonly used
if bytes.IndexByte([]byte{0x21, 0x23, 0x26, 0x2A, 0x2B}, channel[0]) == -1 { // (e.g. ZNC.)
if bytes.IndexByte([]byte{'!', '#', '&', '*', '~', '+'}, channel[0]) == -1 {
return false return false
} }
@ -186,14 +187,14 @@ func IsValidChannel(channel string) bool {
// 1 (prefix) + 5 (id) + 1 (+, channel name) // 1 (prefix) + 5 (id) + 1 (+, channel name)
// On some networks, this may be extended with ISUPPORT capabilities, // On some networks, this may be extended with ISUPPORT capabilities,
// however this is extremely uncommon. // however this is extremely uncommon.
if channel[0] == 0x21 { if channel[0] == '!' {
if len(channel) < 7 { if len(channel) < 7 {
return false return false
} }
// check for valid ID // check for valid ID
for i := 1; i < 6; i++ { for i := 1; i < 6; i++ {
if (channel[i] < 0x30 || channel[i] > 0x39) && (channel[i] < 0x41 || channel[i] > 0x5A) { if (channel[i] < '0' || channel[i] > '9') && (channel[i] < 'A' || channel[i] > 'Z') {
return false return false
} }
} }
@ -222,17 +223,15 @@ func IsValidNick(nick string) bool {
return false return false
} }
nick = ToRFC1459(nick)
// Check the first index. Some characters aren't allowed for the first // Check the first index. Some characters aren't allowed for the first
// index of an IRC nickname. // index of an IRC nickname.
if nick[0] < 0x41 || nick[0] > 0x7D { if (nick[0] < 'A' || nick[0] > '}') && nick[0] != '?' {
// a-z, A-Z, and _\[]{}^| // a-z, A-Z, '_\[]{}^|', and '?' in the case of znc.
return false return false
} }
for i := 1; i < len(nick); i++ { for i := 1; i < len(nick); i++ {
if (nick[i] < 0x41 || nick[i] > 0x7D) && (nick[i] < 0x30 || nick[i] > 0x39) && nick[i] != 0x2D { if (nick[i] < 'A' || nick[i] > '}') && (nick[i] < '0' || nick[i] > '9') && nick[i] != '-' {
// a-z, A-Z, 0-9, -, and _\[]{}^| // a-z, A-Z, 0-9, -, and _\[]{}^|
return false return false
} }
@ -261,10 +260,8 @@ func IsValidUser(name string) bool {
return false return false
} }
name = ToRFC1459(name)
// "~" is prepended (commonly) if there was no ident server response. // "~" is prepended (commonly) if there was no ident server response.
if name[0] == 0x7E { if name[0] == '~' {
// Means name only contained "~". // Means name only contained "~".
if len(name) < 2 { if len(name) < 2 {
return false return false
@ -274,12 +271,12 @@ func IsValidUser(name string) bool {
} }
// Check to see if the first index is alphanumeric. // Check to see if the first index is alphanumeric.
if (name[0] < 0x41 || name[0] > 0x4A) && (name[0] < 0x61 || name[0] > 0x7A) && (name[0] < 0x30 || name[0] > 0x39) { if (name[0] < 'A' || name[0] > 'J') && (name[0] < 'a' || name[0] > 'z') && (name[0] < '0' || name[0] > '9') {
return false return false
} }
for i := 1; i < len(name); i++ { for i := 1; i < len(name); i++ {
if (name[i] < 0x41 || name[i] > 0x7D) && (name[i] < 0x30 || name[i] > 0x39) && name[i] != 0x2D && name[i] != 0x2E { if (name[i] < 'A' || name[i] > '}') && (name[i] < '0' || name[i] > '9') && name[i] != '-' && name[i] != '.' {
// a-z, A-Z, 0-9, -, and _\[]{}^| // a-z, A-Z, 0-9, -, and _\[]{}^|
return false return false
} }
@ -290,8 +287,13 @@ func IsValidUser(name string) bool {
// ToRFC1459 converts a string to the stripped down conversion within RFC // ToRFC1459 converts a string to the stripped down conversion within RFC
// 1459. This will do things like replace an "A" with an "a", "[]" with "{}", // 1459. This will do things like replace an "A" with an "a", "[]" with "{}",
// and so forth. Useful to compare two nicknames or channels. // and so forth. Useful to compare two nicknames or channels. Note that this
func ToRFC1459(input string) (out string) { // should not be used to normalize nicknames or similar, as this may convert
// valid input characters to non-rfc-valid characters. As such, it's main use
// is for comparing two nicks.
func ToRFC1459(input string) string {
var out string
for i := 0; i < len(input); i++ { for i := 0; i < len(input); i++ {
if input[i] >= 65 && input[i] <= 94 { if input[i] >= 65 && input[i] <= 94 {
out += string(rune(input[i]) + 32) out += string(rune(input[i]) + 32)

View File

@ -29,11 +29,12 @@ func (c *Client) RunHandlers(event *Event) {
} }
} }
// Regular wildcard handlers. // Background handlers first.
c.Handlers.exec(ALL_EVENTS, c, event.Copy()) c.Handlers.exec(ALL_EVENTS, true, c, event.Copy())
c.Handlers.exec(event.Command, true, c, event.Copy())
// Then regular handlers. c.Handlers.exec(ALL_EVENTS, false, c, event.Copy())
c.Handlers.exec(event.Command, c, event.Copy()) c.Handlers.exec(event.Command, false, c, event.Copy())
// Check if it's a CTCP. // Check if it's a CTCP.
if ctcp := decodeCTCP(event.Copy()); ctcp != nil { if ctcp := decodeCTCP(event.Copy()); ctcp != nil {
@ -144,7 +145,7 @@ func (c *Caller) cuid(cmd string, n int) (cuid, uid string) {
// cuidToID allows easy mapping between a generated cuid and the caller // cuidToID allows easy mapping between a generated cuid and the caller
// external/internal handler maps. // external/internal handler maps.
func (c *Caller) cuidToID(input string) (cmd, uid string) { func (c *Caller) cuidToID(input string) (cmd, uid string) {
i := strings.IndexByte(input, 0x3A) i := strings.IndexByte(input, ':')
if i < 0 { if i < 0 {
return "", "" return "", ""
} }
@ -160,9 +161,9 @@ type execStack struct {
// exec executes all handlers pertaining to specified event. Internal first, // exec executes all handlers pertaining to specified event. Internal first,
// then external. // then external.
// //
// Please note that there is no specific order/priority for which the // Please note that there is no specific order/priority for which the handlers
// handler types themselves or the handlers are executed. // are executed.
func (c *Caller) exec(command string, client *Client, event *Event) { func (c *Caller) exec(command string, bg bool, client *Client, event *Event) {
// Build a stack of handlers which can be executed concurrently. // Build a stack of handlers which can be executed concurrently.
var stack []execStack var stack []execStack
@ -170,13 +171,21 @@ func (c *Caller) exec(command string, client *Client, event *Event) {
// Get internal handlers first. // Get internal handlers first.
if _, ok := c.internal[command]; ok { if _, ok := c.internal[command]; ok {
for cuid := range c.internal[command] { for cuid := range c.internal[command] {
if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) {
continue
}
stack = append(stack, execStack{c.internal[command][cuid], cuid}) stack = append(stack, execStack{c.internal[command][cuid], cuid})
} }
} }
// Aaand then external handlers. // Then external handlers.
if _, ok := c.external[command]; ok { if _, ok := c.external[command]; ok {
for cuid := range c.external[command] { for cuid := range c.external[command] {
if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) {
continue
}
stack = append(stack, execStack{c.external[command][cuid], cuid}) stack = append(stack, execStack{c.external[command][cuid], cuid})
} }
} }
@ -189,18 +198,29 @@ func (c *Caller) exec(command string, client *Client, event *Event) {
wg.Add(len(stack)) wg.Add(len(stack))
for i := 0; i < len(stack); i++ { for i := 0; i < len(stack); i++ {
go func(index int) { go func(index int) {
c.debug.Printf("executing handler %s for event %s (%d of %d)", stack[index].cuid, command, index+1, len(stack)) defer wg.Done()
c.debug.Printf("[%d/%d] exec %s => %s", index+1, len(stack), stack[index].cuid, command)
start := time.Now() start := time.Now()
// If they want to catch any panics, add to defer stack. if bg {
go func() {
if client.Config.RecoverFunc != nil {
defer recoverHandlerPanic(client, event, stack[index].cuid, 3)
}
stack[index].Execute(client, *event)
c.debug.Printf("[%d/%d] done %s == %s", index+1, len(stack), stack[index].cuid, time.Since(start))
}()
return
}
if client.Config.RecoverFunc != nil { if client.Config.RecoverFunc != nil {
defer recoverHandlerPanic(client, event, stack[index].cuid, 3) defer recoverHandlerPanic(client, event, stack[index].cuid, 3)
} }
stack[index].Execute(client, *event) stack[index].Execute(client, *event)
c.debug.Printf("[%d/%d] done %s == %s", index+1, len(stack), stack[index].cuid, time.Since(start))
c.debug.Printf("execution of %s took %s (%d of %d)", stack[index].cuid, time.Since(start), index+1, len(stack))
wg.Done()
}(i) }(i)
} }
@ -281,9 +301,9 @@ func (c *Caller) remove(cuid string) (success bool) {
// sregister is much like Caller.register(), except that it safely locks // sregister is much like Caller.register(), except that it safely locks
// the Caller mutex. // the Caller mutex.
func (c *Caller) sregister(internal bool, cmd string, handler Handler) (cuid string) { func (c *Caller) sregister(internal, bg bool, cmd string, handler Handler) (cuid string) {
c.mu.Lock() c.mu.Lock()
cuid = c.register(internal, cmd, handler) cuid = c.register(internal, bg, cmd, handler)
c.mu.Unlock() c.mu.Unlock()
return cuid return cuid
@ -291,30 +311,34 @@ func (c *Caller) sregister(internal bool, cmd string, handler Handler) (cuid str
// register will register a handler in the internal tracker. Unsafe (you // register will register a handler in the internal tracker. Unsafe (you
// must lock c.mu yourself!) // must lock c.mu yourself!)
func (c *Caller) register(internal bool, cmd string, handler Handler) (cuid string) { func (c *Caller) register(internal, bg bool, cmd string, handler Handler) (cuid string) {
var uid string var uid string
cmd = strings.ToUpper(cmd) cmd = strings.ToUpper(cmd)
cuid, uid = c.cuid(cmd, 20)
if bg {
uid += ":bg"
cuid += ":bg"
}
if internal { if internal {
if _, ok := c.internal[cmd]; !ok { if _, ok := c.internal[cmd]; !ok {
c.internal[cmd] = map[string]Handler{} c.internal[cmd] = map[string]Handler{}
} }
cuid, uid = c.cuid(cmd, 20)
c.internal[cmd][uid] = handler c.internal[cmd][uid] = handler
} else { } else {
if _, ok := c.external[cmd]; !ok { if _, ok := c.external[cmd]; !ok {
c.external[cmd] = map[string]Handler{} c.external[cmd] = map[string]Handler{}
} }
cuid, uid = c.cuid(cmd, 20)
c.external[cmd][uid] = handler c.external[cmd][uid] = handler
} }
_, file, line, _ := runtime.Caller(3) _, file, line, _ := runtime.Caller(3)
c.debug.Printf("registering handler for %q with cuid %q (internal: %t) from: %s:%d", cmd, cuid, internal, file, line) c.debug.Printf("reg %q => %s [int:%t bg:%t] %s:%d", uid, cmd, internal, bg, file, line)
return cuid return cuid
} }
@ -323,31 +347,20 @@ func (c *Caller) register(internal bool, cmd string, handler Handler) (cuid stri
// given event. cuid is the handler uid which can be used to remove the // given event. cuid is the handler uid which can be used to remove the
// handler with Caller.Remove(). // handler with Caller.Remove().
func (c *Caller) AddHandler(cmd string, handler Handler) (cuid string) { func (c *Caller) AddHandler(cmd string, handler Handler) (cuid string) {
return c.sregister(false, cmd, handler) return c.sregister(false, false, cmd, handler)
} }
// Add registers the handler function for the given event. cuid is the // Add registers the handler function for the given event. cuid is the
// handler uid which can be used to remove the handler with Caller.Remove(). // handler uid which can be used to remove the handler with Caller.Remove().
func (c *Caller) Add(cmd string, handler func(client *Client, event Event)) (cuid string) { func (c *Caller) Add(cmd string, handler func(client *Client, event Event)) (cuid string) {
return c.sregister(false, cmd, HandlerFunc(handler)) return c.sregister(false, false, cmd, HandlerFunc(handler))
} }
// AddBg registers the handler function for the given event and executes it // AddBg registers the handler function for the given event and executes it
// in a go-routine. cuid is the handler uid which can be used to remove the // in a go-routine. cuid is the handler uid which can be used to remove the
// handler with Caller.Remove(). // handler with Caller.Remove().
func (c *Caller) AddBg(cmd string, handler func(client *Client, event Event)) (cuid string) { func (c *Caller) AddBg(cmd string, handler func(client *Client, event Event)) (cuid string) {
return c.sregister(false, cmd, HandlerFunc(func(client *Client, event Event) { return c.sregister(false, true, cmd, HandlerFunc(handler))
// Setting up background-based handlers this way allows us to get
// clean call stacks for use with panic recovery.
go func() {
// If they want to catch any panics, add to defer stack.
if client.Config.RecoverFunc != nil {
defer recoverHandlerPanic(client, &event, "goroutine", 3)
}
handler(client, event)
}()
}))
} }
// AddTmp adds a "temporary" handler, which is good for one-time or few-time // AddTmp adds a "temporary" handler, which is good for one-time or few-time
@ -361,47 +374,37 @@ func (c *Caller) AddBg(cmd string, handler func(client *Client, event Event)) (c
// //
// Additionally, AddTmp has a useful option, deadline. When set to greater // Additionally, AddTmp has a useful option, deadline. When set to greater
// than 0, deadline will be the amount of time that passes before the handler // than 0, deadline will be the amount of time that passes before the handler
// is removed from the stack, regardless if the handler returns true or not. // is removed from the stack, regardless of if the handler returns true or not.
// This is useful in that it ensures that the handler is cleaned up if the // This is useful in that it ensures that the handler is cleaned up if the
// server does not respond appropriately, or takes too long to respond. // server does not respond appropriately, or takes too long to respond.
// //
// Note that handlers supplied with AddTmp are executed in a goroutine to // Note that handlers supplied with AddTmp are executed in a goroutine to
// ensure that they are not blocking other handlers. Additionally, use cuid // ensure that they are not blocking other handlers. However, if you are
// with Caller.Remove() to prematurely remove the handler from the stack, // creating a temporary handler from another handler, it should be a
// bypassing the timeout or waiting for the handler to return that it wants // background handler.
// to be removed from the stack. //
// Use cuid with Caller.Remove() to prematurely remove the handler from the
// stack, bypassing the timeout or waiting for the handler to return that it
// wants to be removed from the stack.
func (c *Caller) AddTmp(cmd string, deadline time.Duration, handler func(client *Client, event Event) bool) (cuid string, done chan struct{}) { func (c *Caller) AddTmp(cmd string, deadline time.Duration, handler func(client *Client, event Event) bool) (cuid string, done chan struct{}) {
var uid string
cuid, uid = c.cuid(cmd, 20)
done = make(chan struct{}) done = make(chan struct{})
c.mu.Lock() cuid = c.sregister(false, true, cmd, HandlerFunc(func(client *Client, event Event) {
if _, ok := c.external[cmd]; !ok { remove := handler(client, event)
c.external[cmd] = map[string]Handler{} if remove {
} if ok := c.Remove(cuid); ok {
c.external[cmd][uid] = HandlerFunc(func(client *Client, event Event) { close(done)
// Setting up background-based handlers this way allows us to get
// clean call stacks for use with panic recovery.
go func() {
// If they want to catch any panics, add to defer stack.
if client.Config.RecoverFunc != nil {
defer recoverHandlerPanic(client, &event, "tmp-goroutine", 3)
} }
}
remove := handler(client, event) }))
if remove {
if ok := c.Remove(cuid); ok {
close(done)
}
}
}()
})
c.mu.Unlock()
if deadline > 0 { if deadline > 0 {
go func() { go func() {
<-time.After(deadline) select {
case <-time.After(deadline):
case <-done:
}
if ok := c.Remove(cuid); ok { if ok := c.Remove(cuid); ok {
close(done) close(done)
} }

View File

@ -206,11 +206,11 @@ func (c *CModes) Parse(flags string, args []string) (out []CMode) {
var argCount int var argCount int
for i := 0; i < len(flags); i++ { for i := 0; i < len(flags); i++ {
if flags[i] == 0x2B { if flags[i] == '+' {
add = true add = true
continue continue
} }
if flags[i] == 0x2D { if flags[i] == '-' {
add = false add = false
continue continue
} }
@ -265,7 +265,7 @@ func IsValidChannelMode(raw string) bool {
for i := 0; i < len(raw); i++ { for i := 0; i < len(raw); i++ {
// Allowed are: ",", A-Z and a-z. // Allowed are: ",", A-Z and a-z.
if raw[i] != 0x2C && (raw[i] < 0x41 || raw[i] > 0x5A) && (raw[i] < 0x61 || raw[i] > 0x7A) { if raw[i] != ',' && (raw[i] < 'A' || raw[i] > 'Z') && (raw[i] < 'a' || raw[i] > 'z') {
return false return false
} }
} }
@ -279,7 +279,7 @@ func isValidUserPrefix(raw string) bool {
return false return false
} }
if raw[0] != 0x28 { // (. if raw[0] != '(' {
return false return false
} }
@ -288,7 +288,7 @@ func isValidUserPrefix(raw string) bool {
// Skip the first one as we know it's (. // Skip the first one as we know it's (.
for i := 1; i < len(raw); i++ { for i := 1; i < len(raw); i++ {
if raw[i] == 0x29 { // ). if raw[i] == ')' {
passedKeys = true passedKeys = true
continue continue
} }

View File

@ -79,7 +79,7 @@ func (cli *Client) BuildBaseURL(urlPath ...string) string {
return hsURL.String() return hsURL.String()
} }
// BuildURLWithQuery builds a URL with query paramters in addition to the Client's homeserver/prefix/access_token set already. // BuildURLWithQuery builds a URL with query parameters in addition to the Client's homeserver/prefix/access_token set already.
func (cli *Client) BuildURLWithQuery(urlPath []string, urlQuery map[string]string) string { func (cli *Client) BuildURLWithQuery(urlPath []string, urlQuery map[string]string) string {
u, _ := url.Parse(cli.BuildURL(urlPath...)) u, _ := url.Parse(cli.BuildURL(urlPath...))
q := u.Query() q := u.Query()
@ -387,6 +387,20 @@ func (cli *Client) JoinRoom(roomIDorAlias, serverName string, content interface{
return return
} }
// GetDisplayName returns the display name of the user from the specified MXID. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
func (cli *Client) GetDisplayName(mxid string) (resp *RespUserDisplayName, err error) {
urlPath := cli.BuildURL("profile", mxid, "displayname")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// GetOwnDisplayName returns the user's display name. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
func (cli *Client) GetOwnDisplayName() (resp *RespUserDisplayName, err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "displayname")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// SetDisplayName sets the user's profile display name. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-displayname // SetDisplayName sets the user's profile display name. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-displayname
func (cli *Client) SetDisplayName(displayName string) (err error) { func (cli *Client) SetDisplayName(displayName string) (err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "displayname") urlPath := cli.BuildURL("profile", cli.UserID, "displayname")
@ -450,6 +464,35 @@ func (cli *Client) SendText(roomID, text string) (*RespSendEvent, error) {
TextMessage{"m.text", text}) TextMessage{"m.text", text})
} }
// SendImage sends an m.room.message event into the given room with a msgtype of m.image
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
func (cli *Client) SendImage(roomID, body, url string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
ImageMessage{
MsgType: "m.image",
Body: body,
URL: url,
})
}
// SendVideo sends an m.room.message event into the given room with a msgtype of m.video
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
func (cli *Client) SendVideo(roomID, body, url string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
VideoMessage{
MsgType: "m.video",
Body: body,
URL: url,
})
}
// SendNotice sends an m.room.message event into the given room with a msgtype of m.notice
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-notice
func (cli *Client) SendNotice(roomID, text string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
TextMessage{"m.notice", text})
}
// RedactEvent redacts the given event. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid // RedactEvent redacts the given event. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid
func (cli *Client) RedactEvent(roomID, eventID string, req *ReqRedact) (resp *RespSendEvent, err error) { func (cli *Client) RedactEvent(roomID, eventID string, req *ReqRedact) (resp *RespSendEvent, err error) {
txnID := txnID() txnID := txnID()
@ -518,6 +561,14 @@ func (cli *Client) UnbanUser(roomID string, req *ReqUnbanUser) (resp *RespUnbanU
return return
} }
// UserTyping sets the typing status of the user. See https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
func (cli *Client) UserTyping(roomID string, typing bool, timeout int64) (resp *RespTyping, err error) {
req := ReqTyping{Typing: typing, Timeout: timeout}
u := cli.BuildURL("rooms", roomID, "typing", cli.UserID)
_, err = cli.MakeRequest("PUT", u, req, &resp)
return
}
// StateEvent gets a single state event in a room. It will attempt to JSON unmarshal into the given "outContent" struct with // StateEvent gets a single state event in a room. It will attempt to JSON unmarshal into the given "outContent" struct with
// the HTTP response body, or return an error. // the HTTP response body, or return an error.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey // See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey
@ -556,8 +607,15 @@ func (cli *Client) UploadToContentRepo(content io.Reader, contentType string, co
return nil, err return nil, err
} }
if res.StatusCode != 200 { if res.StatusCode != 200 {
contents, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, HTTPError{
Message: "Upload request failed - Failed to read response body: " + err.Error(),
Code: res.StatusCode,
}
}
return nil, HTTPError{ return nil, HTTPError{
Message: "Upload request failed", Message: "Upload request failed: " + string(contents),
Code: res.StatusCode, Code: res.StatusCode,
} }
} }
@ -588,6 +646,34 @@ func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) {
return return
} }
// Messages returns a list of message and state events for a room. It uses
// pagination query parameters to paginate history in the room.
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages
func (cli *Client) Messages(roomID, from, to string, dir rune, limit int) (resp *RespMessages, err error) {
query := map[string]string{
"from": from,
"dir": string(dir),
}
if to != "" {
query["to"] = to
}
if limit != 0 {
query["limit"] = strconv.Itoa(limit)
}
urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "messages"}, query)
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// TurnServer returns turn server details and credentials for the client to use when initiating calls.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-voip-turnserver
func (cli *Client) TurnServer() (resp *RespTurnServer, err error) {
urlPath := cli.BuildURL("voip", "turnServer")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
func txnID() string { func txnID() string {
return "go" + strconv.FormatInt(time.Now().UnixNano(), 10) return "go" + strconv.FormatInt(time.Now().UnixNano(), 10)
} }

View File

@ -7,13 +7,13 @@ import (
// Event represents a single Matrix event. // Event represents a single Matrix event.
type Event struct { type Event struct {
StateKey string `json:"state_key"` // The state key for the event. Only present on State Events. StateKey *string `json:"state_key,omitempty"` // The state key for the event. Only present on State Events.
Sender string `json:"sender"` // The user ID of the sender of the event Sender string `json:"sender"` // The user ID of the sender of the event
Type string `json:"type"` // The event type Type string `json:"type"` // The event type
Timestamp int `json:"origin_server_ts"` // The unix timestamp when this message was sent by the origin server Timestamp int64 `json:"origin_server_ts"` // The unix timestamp when this message was sent by the origin server
ID string `json:"event_id"` // The unique ID of this event ID string `json:"event_id"` // The unique ID of this event
RoomID string `json:"room_id"` // The room the event was sent to. May be nil (e.g. for presence) RoomID string `json:"room_id"` // The room the event was sent to. May be nil (e.g. for presence)
Content map[string]interface{} `json:"content"` // The JSON content of the event. Content map[string]interface{} `json:"content"` // The JSON content of the event.
} }
// Body returns the value of the "body" key in the event content if it is // Body returns the value of the "body" key in the event content if it is
@ -44,12 +44,31 @@ type TextMessage struct {
Body string `json:"body"` Body string `json:"body"`
} }
// ImageInfo contains info about an image // ImageInfo contains info about an image - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
type ImageInfo struct { type ImageInfo struct {
Height uint `json:"h"` Height uint `json:"h,omitempty"`
Width uint `json:"w"` Width uint `json:"w,omitempty"`
Mimetype string `json:"mimetype"` Mimetype string `json:"mimetype,omitempty"`
Size uint `json:"size"` Size uint `json:"size,omitempty"`
}
// VideoInfo contains info about a video - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
type VideoInfo struct {
Mimetype string `json:"mimetype,omitempty"`
ThumbnailInfo ImageInfo `json:"thumbnail_info"`
ThumbnailURL string `json:"thumbnail_url,omitempty"`
Height uint `json:"h,omitempty"`
Width uint `json:"w,omitempty"`
Duration uint `json:"duration,omitempty"`
Size uint `json:"size,omitempty"`
}
// VideoMessage is an m.video - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
type VideoMessage struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
URL string `json:"url"`
Info VideoInfo `json:"info"`
} }
// ImageMessage is an m.image event // ImageMessage is an m.image event

43
vendor/github.com/matrix-org/gomatrix/filter.go generated vendored Normal file
View File

@ -0,0 +1,43 @@
// Copyright 2017 Jan Christian Grünhage
//
// 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.
package gomatrix
//Filter is used by clients to specify how the server should filter responses to e.g. sync requests
//Specified by: https://matrix.org/docs/spec/client_server/r0.2.0.html#filtering
type Filter struct {
AccountData FilterPart `json:"account_data,omitempty"`
EventFields []string `json:"event_fields,omitempty"`
EventFormat string `json:"event_format,omitempty"`
Presence FilterPart `json:"presence,omitempty"`
Room struct {
AccountData FilterPart `json:"account_data,omitempty"`
Ephemeral FilterPart `json:"ephemeral,omitempty"`
IncludeLeave bool `json:"include_leave,omitempty"`
NotRooms []string `json:"not_rooms,omitempty"`
Rooms []string `json:"rooms,omitempty"`
State FilterPart `json:"state,omitempty"`
Timeline FilterPart `json:"timeline,omitempty"`
} `json:"room,omitempty"`
}
type FilterPart struct {
NotRooms []string `json:"not_rooms,omitempty"`
Rooms []string `json:"rooms,omitempty"`
Limit *int `json:"limit,omitempty"`
NotSenders []string `json:"not_senders,omitempty"`
NotTypes []string `json:"not_types,omitempty"`
Senders []string `json:"senders,omitempty"`
Types []string `json:"types,omitempty"`
}

View File

@ -70,3 +70,9 @@ type ReqBanUser struct {
type ReqUnbanUser struct { type ReqUnbanUser struct {
UserID string `json:"user_id"` UserID string `json:"user_id"`
} }
// ReqTyping is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
type ReqTyping struct {
Typing bool `json:"typing"`
Timeout int64 `json:"timeout"`
}

View File

@ -45,6 +45,9 @@ type RespBanUser struct{}
// RespUnbanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban // RespUnbanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban
type RespUnbanUser struct{} type RespUnbanUser struct{}
// RespTyping is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
type RespTyping struct{}
// RespJoinedRooms is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680 // RespJoinedRooms is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680
type RespJoinedRooms struct { type RespJoinedRooms struct {
JoinedRooms []string `json:"joined_rooms"` JoinedRooms []string `json:"joined_rooms"`
@ -58,6 +61,13 @@ type RespJoinedMembers struct {
} `json:"joined"` } `json:"joined"`
} }
// RespMessages is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages
type RespMessages struct {
Start string `json:"start"`
Chunk []Event `json:"chunk"`
End string `json:"end"`
}
// RespSendEvent is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid // RespSendEvent is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
type RespSendEvent struct { type RespSendEvent struct {
EventID string `json:"event_id"` EventID string `json:"event_id"`
@ -90,6 +100,11 @@ func (r RespUserInteractive) HasSingleStageFlow(stageName string) bool {
return false return false
} }
// RespUserDisplayName is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
type RespUserDisplayName struct {
DisplayName string `json:"displayname"`
}
// RespRegister is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register // RespRegister is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
type RespRegister struct { type RespRegister struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
@ -125,6 +140,16 @@ type RespSync struct {
Events []Event `json:"events"` Events []Event `json:"events"`
} `json:"presence"` } `json:"presence"`
Rooms struct { Rooms struct {
Leave map[string]struct {
State struct {
Events []Event `json:"events"`
} `json:"state"`
Timeline struct {
Events []Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
} `json:"timeline"`
} `json:"leave"`
Join map[string]struct { Join map[string]struct {
State struct { State struct {
Events []Event `json:"events"` Events []Event `json:"events"`
@ -142,3 +167,10 @@ type RespSync struct {
} `json:"invite"` } `json:"invite"`
} `json:"rooms"` } `json:"rooms"`
} }
type RespTurnServer struct {
Username string `json:"username"`
Password string `json:"password"`
TTL int `json:"ttl"`
URIs []string `json:"uris"`
}

View File

@ -13,7 +13,7 @@ func (room Room) UpdateState(event *Event) {
if !exists { if !exists {
room.State[event.Type] = make(map[string]*Event) room.State[event.Type] = make(map[string]*Event)
} }
room.State[event.Type][event.StateKey] = event room.State[event.Type][*event.StateKey] = event
} }
// GetStateEvent returns the state event for the given type/state_key combo, or nil. // GetStateEvent returns the state event for the given type/state_key combo, or nil.

View File

@ -73,6 +73,16 @@ func (s *DefaultSyncer) ProcessResponse(res *RespSync, since string) (err error)
s.notifyListeners(&event) s.notifyListeners(&event)
} }
} }
for roomID, roomData := range res.Rooms.Leave {
room := s.getOrCreateRoom(roomID)
for _, event := range roomData.Timeline.Events {
if event.StateKey != nil {
event.RoomID = roomID
room.UpdateState(&event)
s.notifyListeners(&event)
}
}
}
return return
} }
@ -102,7 +112,7 @@ func (s *DefaultSyncer) shouldProcessResponse(resp *RespSync, since string) bool
for roomID, roomData := range resp.Rooms.Join { for roomID, roomData := range resp.Rooms.Join {
for i := len(roomData.Timeline.Events) - 1; i >= 0; i-- { for i := len(roomData.Timeline.Events) - 1; i >= 0; i-- {
e := roomData.Timeline.Events[i] e := roomData.Timeline.Events[i]
if e.Type == "m.room.member" && e.StateKey == s.UserID { if e.Type == "m.room.member" && e.StateKey != nil && *e.StateKey == s.UserID {
m := e.Content["membership"] m := e.Content["membership"]
mship, ok := m.(string) mship, ok := m.(string)
if !ok { if !ok {

201
vendor/github.com/matterbridge/gomatrix/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.

703
vendor/github.com/matterbridge/gomatrix/client.go generated vendored Normal file
View File

@ -0,0 +1,703 @@
// Package gomatrix implements the Matrix Client-Server API.
//
// Specification can be found at http://matrix.org/docs/spec/client_server/r0.2.0.html
package gomatrix
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"path"
"strconv"
"sync"
"time"
)
// Client represents a Matrix client.
type Client struct {
HomeserverURL *url.URL // The base homeserver URL
Prefix string // The API prefix eg '/_matrix/client/r0'
UserID string // The user ID of the client. Used for forming HTTP paths which use the client's user ID.
AccessToken string // The access_token for the client.
Client *http.Client // The underlying HTTP client which will be used to make HTTP requests.
Syncer Syncer // The thing which can process /sync responses
Store Storer // The thing which can store rooms/tokens/ids
// The ?user_id= query parameter for application services. This must be set *prior* to calling a method. If this is empty,
// no user_id parameter will be sent.
// See http://matrix.org/docs/spec/application_service/unstable.html#identity-assertion
AppServiceUserID string
syncingMutex sync.Mutex // protects syncingID
syncingID uint32 // Identifies the current Sync. Only one Sync can be active at any given time.
}
// HTTPError An HTTP Error response, which may wrap an underlying native Go Error.
type HTTPError struct {
WrappedError error
Message string
Code int
}
func (e HTTPError) Error() string {
var wrappedErrMsg string
if e.WrappedError != nil {
wrappedErrMsg = e.WrappedError.Error()
}
return fmt.Sprintf("msg=%s code=%d wrapped=%s", e.Message, e.Code, wrappedErrMsg)
}
// BuildURL builds a URL with the Client's homserver/prefix/access_token set already.
func (cli *Client) BuildURL(urlPath ...string) string {
ps := []string{cli.Prefix}
for _, p := range urlPath {
ps = append(ps, p)
}
return cli.BuildBaseURL(ps...)
}
// BuildBaseURL builds a URL with the Client's homeserver/access_token set already. You must
// supply the prefix in the path.
func (cli *Client) BuildBaseURL(urlPath ...string) string {
// copy the URL. Purposefully ignore error as the input is from a valid URL already
hsURL, _ := url.Parse(cli.HomeserverURL.String())
parts := []string{hsURL.Path}
parts = append(parts, urlPath...)
hsURL.Path = path.Join(parts...)
query := hsURL.Query()
if cli.AccessToken != "" {
query.Set("access_token", cli.AccessToken)
}
if cli.AppServiceUserID != "" {
query.Set("user_id", cli.AppServiceUserID)
}
hsURL.RawQuery = query.Encode()
return hsURL.String()
}
// BuildURLWithQuery builds a URL with query parameters in addition to the Client's homeserver/prefix/access_token set already.
func (cli *Client) BuildURLWithQuery(urlPath []string, urlQuery map[string]string) string {
u, _ := url.Parse(cli.BuildURL(urlPath...))
q := u.Query()
for k, v := range urlQuery {
q.Set(k, v)
}
u.RawQuery = q.Encode()
return u.String()
}
// SetCredentials sets the user ID and access token on this client instance.
func (cli *Client) SetCredentials(userID, accessToken string) {
cli.AccessToken = accessToken
cli.UserID = userID
}
// ClearCredentials removes the user ID and access token on this client instance.
func (cli *Client) ClearCredentials() {
cli.AccessToken = ""
cli.UserID = ""
}
// Sync starts syncing with the provided Homeserver. If Sync() is called twice then the first sync will be stopped and the
// error will be nil.
//
// This function will block until a fatal /sync error occurs, so it should almost always be started as a new goroutine.
// Fatal sync errors can be caused by:
// - The failure to create a filter.
// - Client.Syncer.OnFailedSync returning an error in response to a failed sync.
// - Client.Syncer.ProcessResponse returning an error.
// If you wish to continue retrying in spite of these fatal errors, call Sync() again.
func (cli *Client) Sync() error {
// Mark the client as syncing.
// We will keep syncing until the syncing state changes. Either because
// Sync is called or StopSync is called.
syncingID := cli.incrementSyncingID()
nextBatch := cli.Store.LoadNextBatch(cli.UserID)
filterID := cli.Store.LoadFilterID(cli.UserID)
if filterID == "" {
filterJSON := cli.Syncer.GetFilterJSON(cli.UserID)
resFilter, err := cli.CreateFilter(filterJSON)
if err != nil {
return err
}
filterID = resFilter.FilterID
cli.Store.SaveFilterID(cli.UserID, filterID)
}
for {
resSync, err := cli.SyncRequest(30000, nextBatch, filterID, false, "")
if err != nil {
duration, err2 := cli.Syncer.OnFailedSync(resSync, err)
if err2 != nil {
return err2
}
time.Sleep(duration)
continue
}
// Check that the syncing state hasn't changed
// Either because we've stopped syncing or another sync has been started.
// We discard the response from our sync.
if cli.getSyncingID() != syncingID {
return nil
}
// Save the token now *before* processing it. This means it's possible
// to not process some events, but it means that we won't get constantly stuck processing
// a malformed/buggy event which keeps making us panic.
cli.Store.SaveNextBatch(cli.UserID, resSync.NextBatch)
if err = cli.Syncer.ProcessResponse(resSync, nextBatch); err != nil {
return err
}
nextBatch = resSync.NextBatch
}
}
func (cli *Client) incrementSyncingID() uint32 {
cli.syncingMutex.Lock()
defer cli.syncingMutex.Unlock()
cli.syncingID++
return cli.syncingID
}
func (cli *Client) getSyncingID() uint32 {
cli.syncingMutex.Lock()
defer cli.syncingMutex.Unlock()
return cli.syncingID
}
// StopSync stops the ongoing sync started by Sync.
func (cli *Client) StopSync() {
// Advance the syncing state so that any running Syncs will terminate.
cli.incrementSyncingID()
}
// MakeRequest makes a JSON HTTP request to the given URL.
// If "resBody" is not nil, the response body will be json.Unmarshalled into it.
//
// Returns the HTTP body as bytes on 2xx with a nil error. Returns an error if the response is not 2xx along
// with the HTTP body bytes if it got that far. This error is an HTTPError which includes the returned
// HTTP status code and possibly a RespError as the WrappedError, if the HTTP body could be decoded as a RespError.
func (cli *Client) MakeRequest(method string, httpURL string, reqBody interface{}, resBody interface{}) ([]byte, error) {
var req *http.Request
var err error
if reqBody != nil {
var jsonStr []byte
jsonStr, err = json.Marshal(reqBody)
if err != nil {
return nil, err
}
req, err = http.NewRequest(method, httpURL, bytes.NewBuffer(jsonStr))
} else {
req, err = http.NewRequest(method, httpURL, nil)
}
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
res, err := cli.Client.Do(req)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}
contents, err := ioutil.ReadAll(res.Body)
if res.StatusCode/100 != 2 { // not 2xx
var wrap error
var respErr RespError
if _ = json.Unmarshal(contents, &respErr); respErr.ErrCode != "" {
wrap = respErr
}
// If we failed to decode as RespError, don't just drop the HTTP body, include it in the
// HTTP error instead (e.g proxy errors which return HTML).
msg := "Failed to " + method + " JSON to " + req.URL.Path
if wrap == nil {
msg = msg + ": " + string(contents)
}
return contents, HTTPError{
Code: res.StatusCode,
Message: msg,
WrappedError: wrap,
}
}
if err != nil {
return nil, err
}
if resBody != nil {
if err = json.Unmarshal(contents, &resBody); err != nil {
return nil, err
}
}
return contents, nil
}
// CreateFilter makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-user-userid-filter
func (cli *Client) CreateFilter(filter json.RawMessage) (resp *RespCreateFilter, err error) {
urlPath := cli.BuildURL("user", cli.UserID, "filter")
_, err = cli.MakeRequest("POST", urlPath, &filter, &resp)
return
}
// SyncRequest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync
func (cli *Client) SyncRequest(timeout int, since, filterID string, fullState bool, setPresence string) (resp *RespSync, err error) {
query := map[string]string{
"timeout": strconv.Itoa(timeout),
}
if since != "" {
query["since"] = since
}
if filterID != "" {
query["filter"] = filterID
}
if setPresence != "" {
query["set_presence"] = setPresence
}
if fullState {
query["full_state"] = "true"
}
urlPath := cli.BuildURLWithQuery([]string{"sync"}, query)
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
func (cli *Client) register(u string, req *ReqRegister) (resp *RespRegister, uiaResp *RespUserInteractive, err error) {
var bodyBytes []byte
bodyBytes, err = cli.MakeRequest("POST", u, req, nil)
if err != nil {
httpErr, ok := err.(HTTPError)
if !ok { // network error
return
}
if httpErr.Code == 401 {
// body should be RespUserInteractive, if it isn't, fail with the error
err = json.Unmarshal(bodyBytes, &uiaResp)
return
}
return
}
// body should be RespRegister
err = json.Unmarshal(bodyBytes, &resp)
return
}
// Register makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
//
// Registers with kind=user. For kind=guest, see RegisterGuest.
func (cli *Client) Register(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) {
u := cli.BuildURL("register")
return cli.register(u, req)
}
// RegisterGuest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
// with kind=guest.
//
// For kind=user, see Register.
func (cli *Client) RegisterGuest(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) {
query := map[string]string{
"kind": "guest",
}
u := cli.BuildURLWithQuery([]string{"register"}, query)
return cli.register(u, req)
}
// RegisterDummy performs m.login.dummy registration according to https://matrix.org/docs/spec/client_server/r0.2.0.html#dummy-auth
//
// Only a username and password need to be provided on the ReqRegister struct. Most local/developer homeservers will allow registration
// this way. If the homeserver does not, an error is returned.
//
// This does not set credentials on the client instance. See SetCredentials() instead.
//
// res, err := cli.RegisterDummy(&gomatrix.ReqRegister{
// Username: "alice",
// Password: "wonderland",
// })
// if err != nil {
// panic(err)
// }
// token := res.AccessToken
func (cli *Client) RegisterDummy(req *ReqRegister) (*RespRegister, error) {
res, uia, err := cli.Register(req)
if err != nil && uia == nil {
return nil, err
}
if uia != nil && uia.HasSingleStageFlow("m.login.dummy") {
req.Auth = struct {
Type string `json:"type"`
Session string `json:"session,omitempty"`
}{"m.login.dummy", uia.Session}
res, _, err = cli.Register(req)
if err != nil {
return nil, err
}
}
if res == nil {
return nil, fmt.Errorf("registration failed: does this server support m.login.dummy?")
}
return res, nil
}
// Login a user to the homeserver according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login
// This does not set credentials on this client instance. See SetCredentials() instead.
func (cli *Client) Login(req *ReqLogin) (resp *RespLogin, err error) {
urlPath := cli.BuildURL("login")
_, err = cli.MakeRequest("POST", urlPath, req, &resp)
return
}
// Logout the current user. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-logout
// This does not clear the credentials from the client instance. See ClearCredentials() instead.
func (cli *Client) Logout() (resp *RespLogout, err error) {
urlPath := cli.BuildURL("logout")
_, err = cli.MakeRequest("POST", urlPath, nil, &resp)
return
}
// Versions returns the list of supported Matrix versions on this homeserver. See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-versions
func (cli *Client) Versions() (resp *RespVersions, err error) {
urlPath := cli.BuildBaseURL("_matrix", "client", "versions")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// JoinRoom joins the client to a room ID or alias. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-join-roomidoralias
//
// If serverName is specified, this will be added as a query param to instruct the homeserver to join via that server. If content is specified, it will
// be JSON encoded and used as the request body.
func (cli *Client) JoinRoom(roomIDorAlias, serverName string, content interface{}) (resp *RespJoinRoom, err error) {
var urlPath string
if serverName != "" {
urlPath = cli.BuildURLWithQuery([]string{"join", roomIDorAlias}, map[string]string{
"server_name": serverName,
})
} else {
urlPath = cli.BuildURL("join", roomIDorAlias)
}
_, err = cli.MakeRequest("POST", urlPath, content, &resp)
return
}
// GetDisplayName returns the display name of the user from the specified MXID. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
func (cli *Client) GetDisplayName(mxid string) (resp *RespUserDisplayName, err error) {
urlPath := cli.BuildURL("profile", mxid, "displayname")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// GetOwnDisplayName returns the user's display name. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
func (cli *Client) GetOwnDisplayName() (resp *RespUserDisplayName, err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "displayname")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// SetDisplayName sets the user's profile display name. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-displayname
func (cli *Client) SetDisplayName(displayName string) (err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "displayname")
s := struct {
DisplayName string `json:"displayname"`
}{displayName}
_, err = cli.MakeRequest("PUT", urlPath, &s, nil)
return
}
// GetAvatarURL gets the user's avatar URL. See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-avatar-url
func (cli *Client) GetAvatarURL() (url string, err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "avatar_url")
s := struct {
AvatarURL string `json:"avatar_url"`
}{}
_, err = cli.MakeRequest("GET", urlPath, nil, &s)
if err != nil {
return "", err
}
return s.AvatarURL, nil
}
// SetAvatarURL sets the user's avatar URL. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-avatar-url
func (cli *Client) SetAvatarURL(url string) (err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "avatar_url")
s := struct {
AvatarURL string `json:"avatar_url"`
}{url}
_, err = cli.MakeRequest("PUT", urlPath, &s, nil)
if err != nil {
return err
}
return nil
}
// SendMessageEvent sends a message event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
func (cli *Client) SendMessageEvent(roomID string, eventType string, contentJSON interface{}) (resp *RespSendEvent, err error) {
txnID := txnID()
urlPath := cli.BuildURL("rooms", roomID, "send", eventType, txnID)
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
return
}
// SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
func (cli *Client) SendStateEvent(roomID, eventType, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) {
urlPath := cli.BuildURL("rooms", roomID, "state", eventType, stateKey)
_, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
return
}
// SendText sends an m.room.message event into the given room with a msgtype of m.text
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-text
func (cli *Client) SendText(roomID, text string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
TextMessage{"m.text", text})
}
// SendImage sends an m.room.message event into the given room with a msgtype of m.image
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
func (cli *Client) SendImage(roomID, body, url string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
ImageMessage{
MsgType: "m.image",
Body: body,
URL: url,
})
}
// SendVideo sends an m.room.message event into the given room with a msgtype of m.video
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
func (cli *Client) SendVideo(roomID, body, url string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
VideoMessage{
MsgType: "m.video",
Body: body,
URL: url,
})
}
// SendNotice sends an m.room.message event into the given room with a msgtype of m.notice
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-notice
func (cli *Client) SendNotice(roomID, text string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
TextMessage{"m.notice", text})
}
// RedactEvent redacts the given event. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid
func (cli *Client) RedactEvent(roomID, eventID string, req *ReqRedact) (resp *RespSendEvent, err error) {
txnID := txnID()
urlPath := cli.BuildURL("rooms", roomID, "redact", eventID, txnID)
_, err = cli.MakeRequest("PUT", urlPath, req, &resp)
return
}
// CreateRoom creates a new Matrix room. See https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
// resp, err := cli.CreateRoom(&gomatrix.ReqCreateRoom{
// Preset: "public_chat",
// })
// fmt.Println("Room:", resp.RoomID)
func (cli *Client) CreateRoom(req *ReqCreateRoom) (resp *RespCreateRoom, err error) {
urlPath := cli.BuildURL("createRoom")
_, err = cli.MakeRequest("POST", urlPath, req, &resp)
return
}
// LeaveRoom leaves the given room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-leave
func (cli *Client) LeaveRoom(roomID string) (resp *RespLeaveRoom, err error) {
u := cli.BuildURL("rooms", roomID, "leave")
_, err = cli.MakeRequest("POST", u, struct{}{}, &resp)
return
}
// ForgetRoom forgets a room entirely. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-forget
func (cli *Client) ForgetRoom(roomID string) (resp *RespForgetRoom, err error) {
u := cli.BuildURL("rooms", roomID, "forget")
_, err = cli.MakeRequest("POST", u, struct{}{}, &resp)
return
}
// InviteUser invites a user to a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite
func (cli *Client) InviteUser(roomID string, req *ReqInviteUser) (resp *RespInviteUser, err error) {
u := cli.BuildURL("rooms", roomID, "invite")
_, err = cli.MakeRequest("POST", u, struct{}{}, &resp)
return
}
// InviteUserByThirdParty invites a third-party identifier to a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#invite-by-third-party-id-endpoint
func (cli *Client) InviteUserByThirdParty(roomID string, req *ReqInvite3PID) (resp *RespInviteUser, err error) {
u := cli.BuildURL("rooms", roomID, "invite")
_, err = cli.MakeRequest("POST", u, req, &resp)
return
}
// KickUser kicks a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick
func (cli *Client) KickUser(roomID string, req *ReqKickUser) (resp *RespKickUser, err error) {
u := cli.BuildURL("rooms", roomID, "kick")
_, err = cli.MakeRequest("POST", u, req, &resp)
return
}
// BanUser bans a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban
func (cli *Client) BanUser(roomID string, req *ReqBanUser) (resp *RespBanUser, err error) {
u := cli.BuildURL("rooms", roomID, "ban")
_, err = cli.MakeRequest("POST", u, req, &resp)
return
}
// UnbanUser unbans a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban
func (cli *Client) UnbanUser(roomID string, req *ReqUnbanUser) (resp *RespUnbanUser, err error) {
u := cli.BuildURL("rooms", roomID, "unban")
_, err = cli.MakeRequest("POST", u, req, &resp)
return
}
// UserTyping sets the typing status of the user. See https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
func (cli *Client) UserTyping(roomID string, typing bool, timeout int64) (resp *RespTyping, err error) {
req := ReqTyping{Typing: typing, Timeout: timeout}
u := cli.BuildURL("rooms", roomID, "typing", cli.UserID)
_, err = cli.MakeRequest("PUT", u, req, &resp)
return
}
// StateEvent gets a single state event in a room. It will attempt to JSON unmarshal into the given "outContent" struct with
// the HTTP response body, or return an error.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey
func (cli *Client) StateEvent(roomID, eventType, stateKey string, outContent interface{}) (err error) {
u := cli.BuildURL("rooms", roomID, "state", eventType, stateKey)
_, err = cli.MakeRequest("GET", u, nil, outContent)
return
}
// UploadLink uploads an HTTP URL and then returns an MXC URI.
func (cli *Client) UploadLink(link string) (*RespMediaUpload, error) {
res, err := cli.Client.Get(link)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}
return cli.UploadToContentRepo(res.Body, res.Header.Get("Content-Type"), res.ContentLength)
}
// UploadToContentRepo uploads the given bytes to the content repository and returns an MXC URI.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-media-r0-upload
func (cli *Client) UploadToContentRepo(content io.Reader, contentType string, contentLength int64) (*RespMediaUpload, error) {
req, err := http.NewRequest("POST", cli.BuildBaseURL("_matrix/media/r0/upload"), content)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
req.ContentLength = contentLength
res, err := cli.Client.Do(req)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
contents, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, HTTPError{
Message: "Upload request failed - Failed to read response body: " + err.Error(),
Code: res.StatusCode,
}
}
return nil, HTTPError{
Message: "Upload request failed: " + string(contents),
Code: res.StatusCode,
}
}
var m RespMediaUpload
if err := json.NewDecoder(res.Body).Decode(&m); err != nil {
return nil, err
}
return &m, nil
}
// JoinedMembers returns a map of joined room members. See TODO-SPEC. https://github.com/matrix-org/synapse/pull/1680
//
// In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes.
// This API is primarily designed for application services which may want to efficiently look up joined members in a room.
func (cli *Client) JoinedMembers(roomID string) (resp *RespJoinedMembers, err error) {
u := cli.BuildURL("rooms", roomID, "joined_members")
_, err = cli.MakeRequest("GET", u, nil, &resp)
return
}
// JoinedRooms returns a list of rooms which the client is joined to. See TODO-SPEC. https://github.com/matrix-org/synapse/pull/1680
//
// In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes.
// This API is primarily designed for application services which may want to efficiently look up joined rooms.
func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) {
u := cli.BuildURL("joined_rooms")
_, err = cli.MakeRequest("GET", u, nil, &resp)
return
}
// Messages returns a list of message and state events for a room. It uses
// pagination query parameters to paginate history in the room.
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages
func (cli *Client) Messages(roomID, from, to string, dir rune, limit int) (resp *RespMessages, err error) {
query := map[string]string{
"from": from,
"dir": string(dir),
}
if to != "" {
query["to"] = to
}
if limit != 0 {
query["limit"] = strconv.Itoa(limit)
}
urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "messages"}, query)
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// TurnServer returns turn server details and credentials for the client to use when initiating calls.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-voip-turnserver
func (cli *Client) TurnServer() (resp *RespTurnServer, err error) {
urlPath := cli.BuildURL("voip", "turnServer")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
func txnID() string {
return "go" + strconv.FormatInt(time.Now().UnixNano(), 10)
}
// NewClient creates a new Matrix Client ready for syncing
func NewClient(homeserverURL, userID, accessToken string) (*Client, error) {
hsURL, err := url.Parse(homeserverURL)
if err != nil {
return nil, err
}
// By default, use an in-memory store which will never save filter ids / next batch tokens to disk.
// The client will work with this storer: it just won't remember across restarts.
// In practice, a database backend should be used.
store := NewInMemoryStore()
cli := Client{
AccessToken: accessToken,
HomeserverURL: hsURL,
UserID: userID,
Prefix: "/_matrix/client/r0",
Syncer: NewDefaultSyncer(userID, store),
Store: store,
}
// By default, use the default HTTP client.
cli.Client = http.DefaultClient
return &cli, nil
}

102
vendor/github.com/matterbridge/gomatrix/events.go generated vendored Normal file
View File

@ -0,0 +1,102 @@
package gomatrix
import (
"html"
"regexp"
)
// Event represents a single Matrix event.
type Event struct {
StateKey *string `json:"state_key,omitempty"` // The state key for the event. Only present on State Events.
Sender string `json:"sender"` // The user ID of the sender of the event
Type string `json:"type"` // The event type
Timestamp int64 `json:"origin_server_ts"` // The unix timestamp when this message was sent by the origin server
ID string `json:"event_id"` // The unique ID of this event
RoomID string `json:"room_id"` // The room the event was sent to. May be nil (e.g. for presence)
Content map[string]interface{} `json:"content"` // The JSON content of the event.
Redacts string `json:"redacts,omitempty"` // The event ID that was redacted if a m.room.redaction event
}
// Body returns the value of the "body" key in the event content if it is
// present and is a string.
func (event *Event) Body() (body string, ok bool) {
value, exists := event.Content["body"]
if !exists {
return
}
body, ok = value.(string)
return
}
// MessageType returns the value of the "msgtype" key in the event content if
// it is present and is a string.
func (event *Event) MessageType() (msgtype string, ok bool) {
value, exists := event.Content["msgtype"]
if !exists {
return
}
msgtype, ok = value.(string)
return
}
// TextMessage is the contents of a Matrix formated message event.
type TextMessage struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
}
// ImageInfo contains info about an image - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
type ImageInfo struct {
Height uint `json:"h,omitempty"`
Width uint `json:"w,omitempty"`
Mimetype string `json:"mimetype,omitempty"`
Size uint `json:"size,omitempty"`
}
// VideoInfo contains info about a video - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
type VideoInfo struct {
Mimetype string `json:"mimetype,omitempty"`
ThumbnailInfo ImageInfo `json:"thumbnail_info"`
ThumbnailURL string `json:"thumbnail_url,omitempty"`
Height uint `json:"h,omitempty"`
Width uint `json:"w,omitempty"`
Duration uint `json:"duration,omitempty"`
Size uint `json:"size,omitempty"`
}
// VideoMessage is an m.video - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
type VideoMessage struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
URL string `json:"url"`
Info VideoInfo `json:"info"`
}
// ImageMessage is an m.image event
type ImageMessage struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
URL string `json:"url"`
Info ImageInfo `json:"info"`
}
// An HTMLMessage is the contents of a Matrix HTML formated message event.
type HTMLMessage struct {
Body string `json:"body"`
MsgType string `json:"msgtype"`
Format string `json:"format"`
FormattedBody string `json:"formatted_body"`
}
var htmlRegex = regexp.MustCompile("<[^<]+?>")
// GetHTMLMessage returns an HTMLMessage with the body set to a stripped version of the provided HTML, in addition
// to the provided HTML.
func GetHTMLMessage(msgtype, htmlText string) HTMLMessage {
return HTMLMessage{
Body: html.UnescapeString(htmlRegex.ReplaceAllLiteralString(htmlText, "")),
MsgType: msgtype,
Format: "org.matrix.custom.html",
FormattedBody: htmlText,
}
}

43
vendor/github.com/matterbridge/gomatrix/filter.go generated vendored Normal file
View File

@ -0,0 +1,43 @@
// Copyright 2017 Jan Christian Grünhage
//
// 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.
package gomatrix
//Filter is used by clients to specify how the server should filter responses to e.g. sync requests
//Specified by: https://matrix.org/docs/spec/client_server/r0.2.0.html#filtering
type Filter struct {
AccountData FilterPart `json:"account_data,omitempty"`
EventFields []string `json:"event_fields,omitempty"`
EventFormat string `json:"event_format,omitempty"`
Presence FilterPart `json:"presence,omitempty"`
Room struct {
AccountData FilterPart `json:"account_data,omitempty"`
Ephemeral FilterPart `json:"ephemeral,omitempty"`
IncludeLeave bool `json:"include_leave,omitempty"`
NotRooms []string `json:"not_rooms,omitempty"`
Rooms []string `json:"rooms,omitempty"`
State FilterPart `json:"state,omitempty"`
Timeline FilterPart `json:"timeline,omitempty"`
} `json:"room,omitempty"`
}
type FilterPart struct {
NotRooms []string `json:"not_rooms,omitempty"`
Rooms []string `json:"rooms,omitempty"`
Limit *int `json:"limit,omitempty"`
NotSenders []string `json:"not_senders,omitempty"`
NotTypes []string `json:"not_types,omitempty"`
Senders []string `json:"senders,omitempty"`
Types []string `json:"types,omitempty"`
}

78
vendor/github.com/matterbridge/gomatrix/requests.go generated vendored Normal file
View File

@ -0,0 +1,78 @@
package gomatrix
// ReqRegister is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
type ReqRegister struct {
Username string `json:"username,omitempty"`
BindEmail bool `json:"bind_email,omitempty"`
Password string `json:"password,omitempty"`
DeviceID string `json:"device_id,omitempty"`
InitialDeviceDisplayName string `json:"initial_device_display_name"`
Auth interface{} `json:"auth,omitempty"`
}
// ReqLogin is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login
type ReqLogin struct {
Type string `json:"type"`
Password string `json:"password,omitempty"`
Medium string `json:"medium,omitempty"`
User string `json:"user,omitempty"`
Address string `json:"address,omitempty"`
Token string `json:"token,omitempty"`
DeviceID string `json:"device_id,omitempty"`
InitialDeviceDisplayName string `json:"initial_device_display_name,omitempty"`
}
// ReqCreateRoom is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
type ReqCreateRoom struct {
Visibility string `json:"visibility,omitempty"`
RoomAliasName string `json:"room_alias_name,omitempty"`
Name string `json:"name,omitempty"`
Topic string `json:"topic,omitempty"`
Invite []string `json:"invite,omitempty"`
Invite3PID []ReqInvite3PID `json:"invite_3pid,omitempty"`
CreationContent map[string]interface{} `json:"creation_content,omitempty"`
InitialState []Event `json:"initial_state,omitempty"`
Preset string `json:"preset,omitempty"`
IsDirect bool `json:"is_direct,omitempty"`
}
// ReqRedact is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid
type ReqRedact struct {
Reason string `json:"reason,omitempty"`
}
// ReqInvite3PID is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#id57
// It is also a JSON object used in https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
type ReqInvite3PID struct {
IDServer string `json:"id_server"`
Medium string `json:"medium"`
Address string `json:"address"`
}
// ReqInviteUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite
type ReqInviteUser struct {
UserID string `json:"user_id"`
}
// ReqKickUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick
type ReqKickUser struct {
Reason string `json:"reason,omitempty"`
UserID string `json:"user_id"`
}
// ReqBanUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban
type ReqBanUser struct {
Reason string `json:"reason,omitempty"`
UserID string `json:"user_id"`
}
// ReqUnbanUser is the JSON request for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban
type ReqUnbanUser struct {
UserID string `json:"user_id"`
}
// ReqTyping is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
type ReqTyping struct {
Typing bool `json:"typing"`
Timeout int64 `json:"timeout"`
}

176
vendor/github.com/matterbridge/gomatrix/responses.go generated vendored Normal file
View File

@ -0,0 +1,176 @@
package gomatrix
// RespError is the standard JSON error response from Homeservers. It also implements the Golang "error" interface.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#api-standards
type RespError struct {
ErrCode string `json:"errcode"`
Err string `json:"error"`
}
// Error returns the errcode and error message.
func (e RespError) Error() string {
return e.ErrCode + ": " + e.Err
}
// RespCreateFilter is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-user-userid-filter
type RespCreateFilter struct {
FilterID string `json:"filter_id"`
}
// RespVersions is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-versions
type RespVersions struct {
Versions []string `json:"versions"`
}
// RespJoinRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-join
type RespJoinRoom struct {
RoomID string `json:"room_id"`
}
// RespLeaveRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-leave
type RespLeaveRoom struct{}
// RespForgetRoom is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-forget
type RespForgetRoom struct{}
// RespInviteUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite
type RespInviteUser struct{}
// RespKickUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick
type RespKickUser struct{}
// RespBanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban
type RespBanUser struct{}
// RespUnbanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban
type RespUnbanUser struct{}
// RespTyping is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
type RespTyping struct{}
// RespJoinedRooms is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680
type RespJoinedRooms struct {
JoinedRooms []string `json:"joined_rooms"`
}
// RespJoinedMembers is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680
type RespJoinedMembers struct {
Joined map[string]struct {
DisplayName *string `json:"display_name"`
AvatarURL *string `json:"avatar_url"`
} `json:"joined"`
}
// RespMessages is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages
type RespMessages struct {
Start string `json:"start"`
Chunk []Event `json:"chunk"`
End string `json:"end"`
}
// RespSendEvent is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
type RespSendEvent struct {
EventID string `json:"event_id"`
}
// RespMediaUpload is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-media-r0-upload
type RespMediaUpload struct {
ContentURI string `json:"content_uri"`
}
// RespUserInteractive is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#user-interactive-authentication-api
type RespUserInteractive struct {
Flows []struct {
Stages []string `json:"stages"`
} `json:"flows"`
Params map[string]interface{} `json:"params"`
Session string `json:"string"`
Completed []string `json:"completed"`
ErrCode string `json:"errcode"`
Error string `json:"error"`
}
// HasSingleStageFlow returns true if there exists at least 1 Flow with a single stage of stageName.
func (r RespUserInteractive) HasSingleStageFlow(stageName string) bool {
for _, f := range r.Flows {
if len(f.Stages) == 1 && f.Stages[0] == stageName {
return true
}
}
return false
}
// RespUserDisplayName is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
type RespUserDisplayName struct {
DisplayName string `json:"displayname"`
}
// RespRegister is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
type RespRegister struct {
AccessToken string `json:"access_token"`
DeviceID string `json:"device_id"`
HomeServer string `json:"home_server"`
RefreshToken string `json:"refresh_token"`
UserID string `json:"user_id"`
}
// RespLogin is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login
type RespLogin struct {
AccessToken string `json:"access_token"`
DeviceID string `json:"device_id"`
HomeServer string `json:"home_server"`
UserID string `json:"user_id"`
}
// RespLogout is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-logout
type RespLogout struct{}
// RespCreateRoom is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
type RespCreateRoom struct {
RoomID string `json:"room_id"`
}
// RespSync is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync
type RespSync struct {
NextBatch string `json:"next_batch"`
AccountData struct {
Events []Event `json:"events"`
} `json:"account_data"`
Presence struct {
Events []Event `json:"events"`
} `json:"presence"`
Rooms struct {
Leave map[string]struct {
State struct {
Events []Event `json:"events"`
} `json:"state"`
Timeline struct {
Events []Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
} `json:"timeline"`
} `json:"leave"`
Join map[string]struct {
State struct {
Events []Event `json:"events"`
} `json:"state"`
Timeline struct {
Events []Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
} `json:"timeline"`
} `json:"join"`
Invite map[string]struct {
State struct {
Events []Event
} `json:"invite_state"`
} `json:"invite"`
} `json:"rooms"`
}
type RespTurnServer struct {
Username string `json:"username"`
Password string `json:"password"`
TTL int `json:"ttl"`
URIs []string `json:"uris"`
}

50
vendor/github.com/matterbridge/gomatrix/room.go generated vendored Normal file
View File

@ -0,0 +1,50 @@
package gomatrix
// Room represents a single Matrix room.
type Room struct {
ID string
State map[string]map[string]*Event
}
// UpdateState updates the room's current state with the given Event. This will clobber events based
// on the type/state_key combination.
func (room Room) UpdateState(event *Event) {
_, exists := room.State[event.Type]
if !exists {
room.State[event.Type] = make(map[string]*Event)
}
room.State[event.Type][*event.StateKey] = event
}
// GetStateEvent returns the state event for the given type/state_key combo, or nil.
func (room Room) GetStateEvent(eventType string, stateKey string) *Event {
stateEventMap, _ := room.State[eventType]
event, _ := stateEventMap[stateKey]
return event
}
// GetMembershipState returns the membership state of the given user ID in this room. If there is
// no entry for this member, 'leave' is returned for consistency with left users.
func (room Room) GetMembershipState(userID string) string {
state := "leave"
event := room.GetStateEvent("m.room.member", userID)
if event != nil {
membershipState, found := event.Content["membership"]
if found {
mState, isString := membershipState.(string)
if isString {
state = mState
}
}
}
return state
}
// NewRoom creates a new Room with the given ID
func NewRoom(roomID string) *Room {
// Init the State map and return a pointer to the Room
return &Room{
ID: roomID,
State: make(map[string]map[string]*Event),
}
}

65
vendor/github.com/matterbridge/gomatrix/store.go generated vendored Normal file
View File

@ -0,0 +1,65 @@
package gomatrix
// Storer is an interface which must be satisfied to store client data.
//
// You can either write a struct which persists this data to disk, or you can use the
// provided "InMemoryStore" which just keeps data around in-memory which is lost on
// restarts.
type Storer interface {
SaveFilterID(userID, filterID string)
LoadFilterID(userID string) string
SaveNextBatch(userID, nextBatchToken string)
LoadNextBatch(userID string) string
SaveRoom(room *Room)
LoadRoom(roomID string) *Room
}
// InMemoryStore implements the Storer interface.
//
// Everything is persisted in-memory as maps. It is not safe to load/save filter IDs
// or next batch tokens on any goroutine other than the syncing goroutine: the one
// which called Client.Sync().
type InMemoryStore struct {
Filters map[string]string
NextBatch map[string]string
Rooms map[string]*Room
}
// SaveFilterID to memory.
func (s *InMemoryStore) SaveFilterID(userID, filterID string) {
s.Filters[userID] = filterID
}
// LoadFilterID from memory.
func (s *InMemoryStore) LoadFilterID(userID string) string {
return s.Filters[userID]
}
// SaveNextBatch to memory.
func (s *InMemoryStore) SaveNextBatch(userID, nextBatchToken string) {
s.NextBatch[userID] = nextBatchToken
}
// LoadNextBatch from memory.
func (s *InMemoryStore) LoadNextBatch(userID string) string {
return s.NextBatch[userID]
}
// SaveRoom to memory.
func (s *InMemoryStore) SaveRoom(room *Room) {
s.Rooms[room.ID] = room
}
// LoadRoom from memory.
func (s *InMemoryStore) LoadRoom(roomID string) *Room {
return s.Rooms[roomID]
}
// NewInMemoryStore constructs a new InMemoryStore.
func NewInMemoryStore() *InMemoryStore {
return &InMemoryStore{
Filters: make(map[string]string),
NextBatch: make(map[string]string),
Rooms: make(map[string]*Room),
}
}

164
vendor/github.com/matterbridge/gomatrix/sync.go generated vendored Normal file
View File

@ -0,0 +1,164 @@
package gomatrix
import (
"encoding/json"
"fmt"
"runtime/debug"
"time"
)
// Syncer represents an interface that must be satisfied in order to do /sync requests on a client.
type Syncer interface {
// Process the /sync response. The since parameter is the since= value that was used to produce the response.
// This is useful for detecting the very first sync (since=""). If an error is return, Syncing will be stopped
// permanently.
ProcessResponse(resp *RespSync, since string) error
// OnFailedSync returns either the time to wait before retrying or an error to stop syncing permanently.
OnFailedSync(res *RespSync, err error) (time.Duration, error)
// GetFilterJSON for the given user ID. NOT the filter ID.
GetFilterJSON(userID string) json.RawMessage
}
// DefaultSyncer is the default syncing implementation. You can either write your own syncer, or selectively
// replace parts of this default syncer (e.g. the ProcessResponse method). The default syncer uses the observer
// pattern to notify callers about incoming events. See DefaultSyncer.OnEventType for more information.
type DefaultSyncer struct {
UserID string
Store Storer
listeners map[string][]OnEventListener // event type to listeners array
}
// OnEventListener can be used with DefaultSyncer.OnEventType to be informed of incoming events.
type OnEventListener func(*Event)
// NewDefaultSyncer returns an instantiated DefaultSyncer
func NewDefaultSyncer(userID string, store Storer) *DefaultSyncer {
return &DefaultSyncer{
UserID: userID,
Store: store,
listeners: make(map[string][]OnEventListener),
}
}
// ProcessResponse processes the /sync response in a way suitable for bots. "Suitable for bots" means a stream of
// unrepeating events. Returns a fatal error if a listener panics.
func (s *DefaultSyncer) ProcessResponse(res *RespSync, since string) (err error) {
if !s.shouldProcessResponse(res, since) {
return
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("ProcessResponse panicked! userID=%s since=%s panic=%s\n%s", s.UserID, since, r, debug.Stack())
}
}()
for roomID, roomData := range res.Rooms.Join {
room := s.getOrCreateRoom(roomID)
for _, event := range roomData.State.Events {
event.RoomID = roomID
room.UpdateState(&event)
s.notifyListeners(&event)
}
for _, event := range roomData.Timeline.Events {
event.RoomID = roomID
s.notifyListeners(&event)
}
}
for roomID, roomData := range res.Rooms.Invite {
room := s.getOrCreateRoom(roomID)
for _, event := range roomData.State.Events {
event.RoomID = roomID
room.UpdateState(&event)
s.notifyListeners(&event)
}
}
for roomID, roomData := range res.Rooms.Leave {
room := s.getOrCreateRoom(roomID)
for _, event := range roomData.Timeline.Events {
if event.StateKey != nil {
event.RoomID = roomID
room.UpdateState(&event)
s.notifyListeners(&event)
}
}
}
return
}
// OnEventType allows callers to be notified when there are new events for the given event type.
// There are no duplicate checks.
func (s *DefaultSyncer) OnEventType(eventType string, callback OnEventListener) {
_, exists := s.listeners[eventType]
if !exists {
s.listeners[eventType] = []OnEventListener{}
}
s.listeners[eventType] = append(s.listeners[eventType], callback)
}
// shouldProcessResponse returns true if the response should be processed. May modify the response to remove
// stuff that shouldn't be processed.
func (s *DefaultSyncer) shouldProcessResponse(resp *RespSync, since string) bool {
if since == "" {
return false
}
// This is a horrible hack because /sync will return the most recent messages for a room
// as soon as you /join it. We do NOT want to process those events in that particular room
// because they may have already been processed (if you toggle the bot in/out of the room).
//
// Work around this by inspecting each room's timeline and seeing if an m.room.member event for us
// exists and is "join" and then discard processing that room entirely if so.
// TODO: We probably want to process messages from after the last join event in the timeline.
for roomID, roomData := range resp.Rooms.Join {
for i := len(roomData.Timeline.Events) - 1; i >= 0; i-- {
e := roomData.Timeline.Events[i]
if e.Type == "m.room.member" && e.StateKey != nil && *e.StateKey == s.UserID {
m := e.Content["membership"]
mship, ok := m.(string)
if !ok {
continue
}
if mship == "join" {
_, ok := resp.Rooms.Join[roomID]
if !ok {
continue
}
delete(resp.Rooms.Join, roomID) // don't re-process messages
delete(resp.Rooms.Invite, roomID) // don't re-process invites
break
}
}
}
}
return true
}
// getOrCreateRoom must only be called by the Sync() goroutine which calls ProcessResponse()
func (s *DefaultSyncer) getOrCreateRoom(roomID string) *Room {
room := s.Store.LoadRoom(roomID)
if room == nil { // create a new Room
room = NewRoom(roomID)
s.Store.SaveRoom(room)
}
return room
}
func (s *DefaultSyncer) notifyListeners(event *Event) {
listeners, exists := s.listeners[event.Type]
if !exists {
return
}
for _, fn := range listeners {
fn(event)
}
}
// OnFailedSync always returns a 10 second wait period between failed /syncs, never a fatal error.
func (s *DefaultSyncer) OnFailedSync(res *RespSync, err error) (time.Duration, error) {
return 10 * time.Second, nil
}
// GetFilterJSON returns a filter with a timeline limit of 50.
func (s *DefaultSyncer) GetFilterJSON(userID string) json.RawMessage {
return json.RawMessage(`{"room":{"timeline":{"limit":50}}}`)
}

130
vendor/github.com/matterbridge/gomatrix/userids.go generated vendored Normal file
View File

@ -0,0 +1,130 @@
package gomatrix
import (
"bytes"
"encoding/hex"
"fmt"
"strings"
)
const lowerhex = "0123456789abcdef"
// encode the given byte using quoted-printable encoding (e.g "=2f")
// and writes it to the buffer
// See https://golang.org/src/mime/quotedprintable/writer.go
func encode(buf *bytes.Buffer, b byte) {
buf.WriteByte('=')
buf.WriteByte(lowerhex[b>>4])
buf.WriteByte(lowerhex[b&0x0f])
}
// escape the given alpha character and writes it to the buffer
func escape(buf *bytes.Buffer, b byte) {
buf.WriteByte('_')
if b == '_' {
buf.WriteByte('_') // another _
} else {
buf.WriteByte(b + 0x20) // ASCII shift A-Z to a-z
}
}
func shouldEncode(b byte) bool {
return b != '-' && b != '.' && b != '_' && !(b >= '0' && b <= '9') && !(b >= 'a' && b <= 'z') && !(b >= 'A' && b <= 'Z')
}
func shouldEscape(b byte) bool {
return (b >= 'A' && b <= 'Z') || b == '_'
}
func isValidByte(b byte) bool {
return isValidEscapedChar(b) || (b >= '0' && b <= '9') || b == '.' || b == '=' || b == '-'
}
func isValidEscapedChar(b byte) bool {
return b == '_' || (b >= 'a' && b <= 'z')
}
// EncodeUserLocalpart encodes the given string into Matrix-compliant user ID localpart form.
// See http://matrix.org/docs/spec/intro.html#mapping-from-other-character-sets
//
// This returns a string with only the characters "a-z0-9._=-". The uppercase range A-Z
// are encoded using leading underscores ("_"). Characters outside the aforementioned ranges
// (including literal underscores ("_") and equals ("=")) are encoded as UTF8 code points (NOT NCRs)
// and converted to lower-case hex with a leading "=". For example:
// Alph@Bet_50up => _alph=40_bet=5f50up
func EncodeUserLocalpart(str string) string {
strBytes := []byte(str)
var outputBuffer bytes.Buffer
for _, b := range strBytes {
if shouldEncode(b) {
encode(&outputBuffer, b)
} else if shouldEscape(b) {
escape(&outputBuffer, b)
} else {
outputBuffer.WriteByte(b)
}
}
return outputBuffer.String()
}
// DecodeUserLocalpart decodes the given string back into the original input string.
// Returns an error if the given string is not a valid user ID localpart encoding.
// See http://matrix.org/docs/spec/intro.html#mapping-from-other-character-sets
//
// This decodes quoted-printable bytes back into UTF8, and unescapes casing. For
// example:
// _alph=40_bet=5f50up => Alph@Bet_50up
// Returns an error if the input string contains characters outside the
// range "a-z0-9._=-", has an invalid quote-printable byte (e.g. not hex), or has
// an invalid _ escaped byte (e.g. "_5").
func DecodeUserLocalpart(str string) (string, error) {
strBytes := []byte(str)
var outputBuffer bytes.Buffer
for i := 0; i < len(strBytes); i++ {
b := strBytes[i]
if !isValidByte(b) {
return "", fmt.Errorf("Byte pos %d: Invalid byte", i)
}
if b == '_' { // next byte is a-z and should be upper-case or is another _ and should be a literal _
if i+1 >= len(strBytes) {
return "", fmt.Errorf("Byte pos %d: expected _[a-z_] encoding but ran out of string", i)
}
if !isValidEscapedChar(strBytes[i+1]) { // invalid escaping
return "", fmt.Errorf("Byte pos %d: expected _[a-z_] encoding", i)
}
if strBytes[i+1] == '_' {
outputBuffer.WriteByte('_')
} else {
outputBuffer.WriteByte(strBytes[i+1] - 0x20) // ASCII shift a-z to A-Z
}
i++ // skip next byte since we just handled it
} else if b == '=' { // next 2 bytes are hex and should be buffered ready to be read as utf8
if i+2 >= len(strBytes) {
return "", fmt.Errorf("Byte pos: %d: expected quote-printable encoding but ran out of string", i)
}
dst := make([]byte, 1)
_, err := hex.Decode(dst, strBytes[i+1:i+3])
if err != nil {
return "", err
}
outputBuffer.WriteByte(dst[0])
i += 2 // skip next 2 bytes since we just handled it
} else { // pass through
outputBuffer.WriteByte(b)
}
}
return outputBuffer.String(), nil
}
// ExtractUserLocalpart extracts the localpart portion of a user ID.
// See http://matrix.org/docs/spec/intro.html#user-identifiers
func ExtractUserLocalpart(userID string) (string, error) {
if len(userID) == 0 || userID[0] != '@' {
return "", fmt.Errorf("%s is not a valid user id", userID)
}
return strings.TrimPrefix(
strings.SplitN(userID, ":", 2)[0], // @foo:bar:8448 => [ "@foo", "bar:8448" ]
"@", // remove "@" prefix
), nil
}

View File

@ -1,20 +0,0 @@
package slack
import (
"net"
"net/url"
)
var portMapping = map[string]string{"ws": "80", "wss": "443"}
func websocketizeURLPort(orig string) (string, error) {
urlObj, err := url.ParseRequestURI(orig)
if err != nil {
return "", err
}
_, _, err = net.SplitHostPort(urlObj.Host)
if err != nil {
return urlObj.Scheme + "://" + urlObj.Host + ":" + portMapping[urlObj.Scheme] + urlObj.Path, nil
}
return orig, nil
}

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